ComponentsChangelogPro

    Getting Started

    Animated Cards

    Components

    Backgrounds

    3d & Shaders

    Text Animations

    Buttons

    Docs
    Pixel Distorsion Shader

    Pixel Distorsion Shader

    An interactive and customizable shader effect that distorts pixels dynamically based on mouse interactions, perfect for creating engaging and immersive visual effects on images.

    Loading...

    Installation

    Install dependencies

    npm install

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

    Add the shader file

    components/shaders/pixel-distorsion-shader

    import React, { useRef, useEffect, useState } from "react";
    import { fragment, vertex } from "@/components/shaders/pixel-distorsion-shader";
    import { useFrame, useThree } from "@react-three/fiber";
    import { useTexture } from "@react-three/drei";
    import * as THREE from "three";
     
    export default function PixelDistorsion({
      imagePath,
      cameraDistance = 700,
      grid = 10,
      mouse = 0.25,
      strength = 0.11,
    }) {
      const plane = useRef();
      const texture = useTexture(imagePath);
      const { camera, size } = useThree();
      const [planeSize, setPlaneSize] = useState([1, 1]);
     
      const mouseState = useRef({ x: 0.5, y: 0.5, vX: 0, vY: 0 });
     
      const uniforms = useRef({
        uTexture: { value: texture },
        uTime: { value: 0 },
        uHover: { value: new THREE.Vector2(0.5, 0.5) },
        uDataTexture: { value: null },
        resolution: { value: new THREE.Vector4() },
      });
     
      const generateDataTexture = () => {
        const data = new Float32Array(grid * grid * 4);
     
        for (let i = 0; i < grid * grid; i++) {
          const stride = i * 4;
          data[stride] = Math.random();
          data[stride + 1] = Math.random();
          data[stride + 2] = Math.random();
          data[stride + 3] = 1.0;
        }
     
        const dataTexture = new THREE.DataTexture(
          data,
          grid,
          grid,
          THREE.RGBAFormat,
          THREE.FloatType
        );
        dataTexture.needsUpdate = true;
        dataTexture.minFilter = THREE.NearestFilter;
        dataTexture.magFilter = THREE.NearestFilter;
     
        return dataTexture;
      };
     
      const settings = {
        grid: grid,
        mouseInfluence: mouse,
        strength: strength,
        relaxation: 0.9,
      };
     
      const clamp = (number, min, max) => Math.max(min, Math.min(number, max));
      const updateDataTexture = () => {
        const data = uniforms.current.uDataTexture.value.image.data;
        const gridMouseX = mouseState.current.x * settings.grid;
        const gridMouseY = (1 - mouseState.current.y) * settings.grid;
        const maxDist = settings.grid * settings.mouseInfluence;
        const aspect = size.height / size.width;
     
        for (let i = 0; i < data.length; i += 4) {
          data[i] *= settings.relaxation;
          data[i + 1] *= settings.relaxation;
        }
     
        for (let i = 0; i < settings.grid; i++) {
          for (let j = 0; j < settings.grid; j++) {
            const distance = (gridMouseX - i) ** 2 / aspect + (gridMouseY - j) ** 2;
            const maxDistSq = maxDist ** 2;
     
            if (distance < maxDistSq) {
              const index = 4 * (i + settings.grid * j);
              let power = maxDist / Math.sqrt(distance);
              power = clamp(power, 0, 10);
     
              data[index] +=
                settings.strength * mouseState.current.vX * power * 100;
              data[index + 1] -=
                settings.strength * mouseState.current.vY * power * 100;
            }
          }
        }
        mouseState.current.vX *= 0.9;
        mouseState.current.vY *= 0.9;
     
        uniforms.current.uDataTexture.value.needsUpdate = true;
      };
     
      useEffect(() => {
        uniforms.current.uDataTexture.value = generateDataTexture();
      }, []);
     
      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();
     
          uniforms.current.resolution.value.set(width, size.height, 1, 1);
          plane.current.scale.set(1, 1, 1);
        };
     
        adjustCameraAndPlaneSize();
        window.addEventListener("resize", adjustCameraAndPlaneSize);
     
        return () => {
          window.removeEventListener("resize", adjustCameraAndPlaneSize);
        };
      }, [size, texture, camera]);
     
      const handlePointerMove = (event) => {
        const { offsetX, offsetY } = event.nativeEvent;
        mouseState.current.vX = offsetX / size.width - mouseState.current.x;
        mouseState.current.vY = offsetY / size.height - mouseState.current.y;
        mouseState.current.x = offsetX / size.width;
        mouseState.current.y = offsetY / size.height;
      };
     
      useFrame((state) => {
        plane.current.material.uniforms.uTime.value = state.clock.elapsedTime;
        updateDataTexture();
      });
     
      return (
        <mesh ref={plane} onPointerMove={handlePointerMove}>
          <planeGeometry args={[planeSize[0], planeSize[1], 45, 45]} />
          <shaderMaterial
            side={THREE.DoubleSide}
            vertexShader={vertex}
            fragmentShader={fragment}
            uniforms={uniforms.current}
          />
        </mesh>
      );
    }

    Copy the pixel-distorsion component

    pixel-distorsion.tsx

    import React, { useRef, useEffect, useState } from "react";
    import { fragment, vertex } from "@/components/shaders/pixel-distorsion-shader";
    import { useFrame, useThree } from "@react-three/fiber";
    import { useTexture } from "@react-three/drei";
    import * as THREE from "three";
     
    interface PixelDistorsionProps {
      imagePath: string;
      cameraDistance?: number;
      grid?: number;
      mouse?: number;
      strength?: number;
    }
     
    export default function PixelDistorsion({
      imagePath,
      cameraDistance = 700,
      grid = 10,
      mouse = 0.25,
      strength = 0.11,
    }: PixelDistorsionProps) {
      const plane = useRef<THREE.Mesh>(null);
      const texture = useTexture(imagePath);
      const { camera, size } = useThree();
      const [planeSize, setPlaneSize] = useState<[number, number]>([1, 1]);
     
      const mouseState = useRef({ x: 0.5, y: 0.5, vX: 0, vY: 0 });
     
      const uniforms = useRef({
        uTexture: { value: texture },
        uTime: { value: 0 },
        uHover: { value: new THREE.Vector2(0.5, 0.5) },
        uDataTexture: { value: null },
        resolution: { value: new THREE.Vector4() },
      });
     
      const generateDataTexture = () => {
        const data = new Float32Array(grid * grid * 4);
     
        for (let i = 0; i < grid * grid; i++) {
          const stride = i * 4;
          data[stride] = Math.random();
          data[stride + 1] = Math.random();
          data[stride + 2] = Math.random();
          data[stride + 3] = 1.0;
        }
     
        const dataTexture = new THREE.DataTexture(
          data,
          grid,
          grid,
          THREE.RGBAFormat,
          THREE.FloatType
        );
        dataTexture.needsUpdate = true;
        dataTexture.minFilter = THREE.NearestFilter;
        dataTexture.magFilter = THREE.NearestFilter;
     
        return dataTexture;
      };
     
      const settings = {
        grid,
        mouseInfluence: mouse,
        strength,
        relaxation: 0.9,
      };
     
      const clamp = (number: number, min: number, max: number) =>
        Math.max(min, Math.min(number, max));
      const updateDataTexture = () => {
        const data = uniforms.current.uDataTexture!.value.image.data;
        const gridMouseX = mouseState.current.x * settings.grid;
        const gridMouseY = (1 - mouseState.current.y) * settings.grid;
        const maxDist = settings.grid * settings.mouseInfluence;
        const aspect = size.height / size.width;
     
        for (let i = 0; i < data.length; i += 4) {
          data[i] *= settings.relaxation;
          data[i + 1] *= settings.relaxation;
        }
     
        for (let i = 0; i < settings.grid; i++) {
          for (let j = 0; j < settings.grid; j++) {
            const distance = (gridMouseX - i) ** 2 / aspect + (gridMouseY - j) ** 2;
            const maxDistSq = maxDist ** 2;
     
            if (distance < maxDistSq) {
              const index = 4 * (i + settings.grid * j);
              let power = maxDist / Math.sqrt(distance);
              power = clamp(power, 0, 10);
     
              data[index] +=
                settings.strength * mouseState.current.vX * power * 100;
              data[index + 1] -=
                settings.strength * mouseState.current.vY * power * 100;
            }
          }
        }
        mouseState.current.vX *= 0.9;
        mouseState.current.vY *= 0.9;
     
        uniforms.current.uDataTexture!.value.needsUpdate = true;
      };
     
      useEffect(() => {
        uniforms.current.uDataTexture.value = generateDataTexture();
      }, []);
     
      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();
     
          uniforms.current.resolution.value.set(width, size.height, 1, 1);
          plane.current!.scale.set(1, 1, 1);
        };
     
        adjustCameraAndPlaneSize();
        window.addEventListener("resize", adjustCameraAndPlaneSize);
     
        return () => {
          window.removeEventListener("resize", adjustCameraAndPlaneSize);
        };
      }, [size, texture, camera]);
     
      const handlePointerMove = (event: React.PointerEvent) => {
        const { offsetX, offsetY } = event.nativeEvent;
        mouseState.current.vX = offsetX / size.width - mouseState.current.x;
        mouseState.current.vY = offsetY / size.height - mouseState.current.y;
        mouseState.current.x = offsetX / size.width;
        mouseState.current.y = offsetY / size.height;
      };
     
      useFrame((state) => {
        if (plane.current) {
          (plane.current.material as THREE.ShaderMaterial).uniforms.uTime.value =
            state.clock.elapsedTime;
          updateDataTexture();
        }
      });
     
      return (
        <mesh ref={plane} onPointerMove={handlePointerMove}>
          <planeGeometry args={[planeSize[0], planeSize[1], 45, 45]} />
          <shaderMaterial
            side={THREE.DoubleSide}
            vertexShader={vertex}
            fragmentShader={fragment}
            uniforms={uniforms.current}
          />
        </mesh>
      );
    }
     

    Copy the source code

    pixel-distorsion-scene.tsx

    import React from "react";
    import PixelDistorsion from "@/components/pixel-distorsion";
    import { Canvas } from "@react-three/fiber";
     
    interface PixelDistorsionSceneProps {
      imagePath: string;
      cameraDistance?: number;
    }
     
    export default function PixelDistorsionScene({
      imagePath,
      cameraDistance,
    }: PixelDistorsionSceneProps) {
      return (
        <Canvas>
          <PixelDistorsion imagePath={imagePath} cameraDistance={cameraDistance} />
        </Canvas>
      );
    }

    Props

    PixelDistorsion Props

    PropTypeDescriptionDefault
    imagePathStringThe path to the image that will be used as the texture.-
    cameraDistanceNumberThe distance of the camera from the object.700
    gridNumberDefines the grid size used for pixel distortion.10
    mouseNumberControls the influence of the mouse interaction on distortion.0.25
    strengthNumberDefines the strength of the distortion effect.0.11

    PixelDistorsionScene Props

    PropTypeDescriptionDefault
    imagePathStringThe path to the image that will be passed to PixelDistorsion.-
    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 images are from ShadcnUI
  5. 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