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>
);
}