ComponentsChangelogPro

    Getting Started

    Animated Cards

    Components

    Backgrounds

    3d & Shaders

    Text Animations

    Buttons

    Docs
    Hover Wave Shader

    Hover Wave Shader

    A interactive and customizable shader effect that generates dynamic waves based on hover interactions, ideal for adding engaging visual effects to backgrounds.

    Loading...

    Installation

    Install dependencies

    npm install

    npm install @react-three/fiber @react-three/drei gsap

    Add the shader file

    components/shaders/mouse-wave-shader

    // Classic 3D Perlin Noise created by Stefan Gustavson
     
    export const vertex = `
    vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
    vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
    vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
     
    float cnoise(vec3 P){
      vec3 Pi0 = floor(P); 
      vec3 Pi1 = Pi0 + vec3(1.0);
      Pi0 = mod(Pi0, 289.0);
      Pi1 = mod(Pi1, 289.0);
      vec3 Pf0 = fract(P);
      vec3 Pf1 = Pf0 - vec3(1.0);
      vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
      vec4 iy = vec4(Pi0.yy, Pi1.yy);
      vec4 iz0 = Pi0.zzzz;
      vec4 iz1 = Pi1.zzzz;
     
      vec4 ixy = permute(permute(ix) + iy);
      vec4 ixy0 = permute(ixy + iz0);
      vec4 ixy1 = permute(ixy + iz1);
     
      vec4 gx0 = ixy0 / 7.0;
      vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
      gx0 = fract(gx0);
      vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
      vec4 sz0 = step(gz0, vec4(0.0));
      gx0 -= sz0 * (step(0.0, gx0) - 0.5);
      gy0 -= sz0 * (step(0.0, gy0) - 0.5);
     
      vec4 gx1 = ixy1 / 7.0;
      vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
      gx1 = fract(gx1);
      vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
      vec4 sz1 = step(gz1, vec4(0.0));
      gx1 -= sz1 * (step(0.0, gx1) - 0.5);
      gy1 -= sz1 * (step(0.0, gy1) - 0.5);
     
      vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
      vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
      vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
      vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
      vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
      vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
      vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
      vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
     
      vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
      g000 *= norm0.x;
      g010 *= norm0.y;
      g100 *= norm0.z;
      g110 *= norm0.w;
      vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
      g001 *= norm1.x;
      g011 *= norm1.y;
      g101 *= norm1.z;
      g111 *= norm1.w;
     
      float n000 = dot(g000, Pf0);
      float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
      float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
      float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
      float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
      float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
      float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
      float n111 = dot(g111, Pf1);
     
      vec3 fade_xyz = fade(Pf0);
      vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
      vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
      float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 
      return 2.2 * n_xyz;
    }
     
     
      uniform float uTime;
      uniform vec2 uHover;
      uniform float uHoverState;
      
      varying float vNoise;
      varying vec2  vUv;
      
     
      void main() {
        vec3 newposition = position;
        float PI = 3.1415925;
     
        float noise = cnoise(3.*vec3(position.x,position.y,position.z + uTime/30.));
     
        float dist = distance(uv, uHover);
     
        newposition.z += uHoverState*10.*sin(dist*10. + uTime);
        
        vNoise = uHoverState*sin(dist*10. - uTime);
        vUv = uv;
     
        gl_Position = projectionMatrix * modelViewMatrix * vec4( newposition, 1.0);
      }
    `
     
    export const fragment =`
     
    varying float vNoise;
    varying vec2 vUv;
    uniform sampler2D uTexture;
    uniform float uHoverState;
    uniform float uTime;
     
      void main() {
      vec2 newUV = vUv;
     
      vec2 p = newUV;
      float x = uHoverState;
      x = smoothstep(.0,1.0,(x*2.0+p.y-1.0));
      vec4 f = mix(
        texture2D(uTexture, (p-.5)*(1.-x)+.5), 
        texture2D(uTexture, (p-.5)*x+.5), 
        x);
     
      vec4 color = texture2D(uTexture, vUv);
        gl_FragColor = f;
      }
    `

    Copy the hover-wave component

    hover-wave.tsx

    import React, { useRef, useEffect, useState } from "react";
    import { fragment, vertex } from "@/components/shaders/hover-wave-shader";
    import { useFrame, useThree } from "@react-three/fiber";
    import { useTexture } from "@react-three/drei";
    import * as THREE from "three";
    import gsap from "gsap";
     
    interface HoverWaveProps {
      imagePath: string;
      cameraDistance?: number;
    }
     
    export default function HoverWave({
      imagePath,
      cameraDistance = 700,
    }: HoverWaveProps) {
      const plane = useRef<THREE.Mesh>(null);
      const texture = useTexture(imagePath);
      const { raycaster, camera, size } = useThree();
      const [planeSize, setPlaneSize] = useState<[number, number]>([1, 1]);
     
      const uniforms = useRef({
        uTexture: { value: texture },
        uTime: { value: 0 },
        uHover: { value: new THREE.Vector2(0.5, 0.5) },
        uHoverState: { value: 0 },
      });
     
      useEffect(() => {
        const adjustCameraAndPlaneSize = () => {
          const { width } = size;
     
          const imageRatio = texture.image.width / texture.image.height;
     
          const planeWidth = width / 2;
          const planeHeight = planeWidth / imageRatio;
     
          setPlaneSize([planeWidth, planeHeight]);
     
          const fov = (2 * Math.atan(planeHeight / 2 / 600) * 180) / Math.PI;
          camera.fov = fov;
          camera.position.z = cameraDistance;
          camera.updateProjectionMatrix();
        };
     
        adjustCameraAndPlaneSize();
        window.addEventListener("resize", adjustCameraAndPlaneSize);
     
        return () => {
          window.removeEventListener("resize", adjustCameraAndPlaneSize);
        };
      }, [size, camera]);
     
      useFrame((state) => {
        if (plane.current) {
          (plane.current.material as THREE.ShaderMaterial).uniforms.uTime.value =
            state.clock.elapsedTime;
        }
      });
     
      const handlePointerMove = (event: THREE.Event) => {
        raycaster.setFromCamera(event.pointer, camera);
        const intersects = raycaster.intersectObject(plane.current!);
     
        if (intersects.length > 0) {
          const obj = intersects[0].object as THREE.Mesh;
          (obj.material as THREE.ShaderMaterial).uniforms.uHover.value =
            intersects[0].uv;
        }
      };
     
      const handlePointerEnter = () => {
        gsap.to(uniforms.current.uHoverState, {
          duration: 1,
          value: 1,
        });
      };
     
      const handlePointerLeave = () => {
        gsap.to(uniforms.current.uHoverState, {
          duration: 1,
          value: 0,
        });
      };
     
      return (
        <mesh
          ref={plane}
          onPointerMove={handlePointerMove}
          onPointerEnter={handlePointerEnter}
          onPointerLeave={handlePointerLeave}
        >
          <planeGeometry args={[planeSize[0], planeSize[1], 45, 45]} />
          <shaderMaterial
            side={THREE.DoubleSide}
            vertexShader={vertex}
            fragmentShader={fragment}
            uniforms={uniforms.current}
          />
        </mesh>
      );
    }

    Copy the source code

    hover-wave-scene.tsx

    Props

    HoverWave Props

    PropTypeDescriptionDefault
    imagePathStringThe path to the image that will be used as the texture.-
    cameraDistanceNumberThe distance of the camera from the object.700

    HoverWaveScene Props

    PropTypeDescriptionDefault
    imagePathStringThe path to the image that will be passed to HoverWave.-
    cameraDistanceNumberThe distance of the camera from the object.-

    Note

  1. If you're using Next.js, make sure to use dynamic import for the hover-wave-scene component. You can also add a placeholder during the loading phase if needed.
  2. The component should be updated to make customizing the effect easier for users. In the meantime, if you want to experiment, I recommend using "Leva" for real-time adjustments.
  3. Credits

  4. The shader used in this component was created by Stefan Gustavson @stegu
  5. The images are from ShadcnUI
  6. I drew a lot of inspiration from Akella's work for the creation of these components. You can find plenty of shaders here if you'd like to experiment @akella