215 lines
5.8 KiB
TypeScript
215 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import { Mesh, Plane, Program, Renderer, Texture } from "ogl";
|
|
|
|
const vertex = `
|
|
attribute vec2 uv;
|
|
attribute vec2 position;
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = vec4(position, 0.0, 1.0);
|
|
}
|
|
`;
|
|
|
|
const fragment = `
|
|
precision highp float;
|
|
|
|
uniform sampler2D tMap;
|
|
uniform vec2 uResolution;
|
|
uniform vec2 uImageResolution;
|
|
uniform vec2 uPointer;
|
|
uniform vec2 uVelocity;
|
|
uniform float uStrength;
|
|
varying vec2 vUv;
|
|
|
|
vec2 coverUv(vec2 uv, vec2 screen, vec2 image) {
|
|
float screenRatio = screen.x / screen.y;
|
|
float imageRatio = image.x / image.y;
|
|
vec2 scale = vec2(1.0);
|
|
|
|
if (screenRatio > imageRatio) {
|
|
scale.y = imageRatio / screenRatio;
|
|
} else {
|
|
scale.x = screenRatio / imageRatio;
|
|
}
|
|
|
|
return (uv - 0.5) * scale + 0.5;
|
|
}
|
|
|
|
void main() {
|
|
vec2 uv = coverUv(vUv, uResolution, uImageResolution);
|
|
|
|
vec2 pointerDelta = uv - uPointer;
|
|
float distanceFromPointer = length(pointerDelta);
|
|
float cursorMask = smoothstep(0.36, 0.0, distanceFromPointer);
|
|
float innerMask = smoothstep(0.16, 0.0, distanceFromPointer);
|
|
vec2 pull = normalize(pointerDelta + 0.0001) * innerMask * 0.0035;
|
|
vec2 distortion = ((uVelocity * 0.075) - pull) * cursorMask * uStrength;
|
|
|
|
uv += distortion;
|
|
|
|
float red = texture2D(tMap, uv + distortion * 0.12).r;
|
|
float green = texture2D(tMap, uv).g;
|
|
float blue = texture2D(tMap, uv - distortion * 0.1).b;
|
|
gl_FragColor = vec4(red, green, blue, 1.0);
|
|
}
|
|
`;
|
|
|
|
interface HeroImageSceneProps {
|
|
imageSrc: string;
|
|
}
|
|
|
|
export function HeroImageScene({ imageSrc }: HeroImageSceneProps) {
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
const pointerRef = useRef({ x: 0.5, y: 0.5 });
|
|
const velocityRef = useRef({ x: 0, y: 0 });
|
|
const strengthRef = useRef(0);
|
|
const lastMoveTimeRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
const root = rootRef.current;
|
|
if (!root) return;
|
|
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
if (reduceMotion.matches) return;
|
|
|
|
const renderer = new Renderer({
|
|
alpha: true,
|
|
antialias: true,
|
|
dpr: Math.min(window.devicePixelRatio, 2),
|
|
});
|
|
const gl = renderer.gl;
|
|
root.appendChild(gl.canvas);
|
|
|
|
gl.canvas.className = "absolute inset-0 h-full w-full";
|
|
gl.clearColor(0, 0, 0, 0);
|
|
|
|
const texture = new Texture(gl, {
|
|
generateMipmaps: false,
|
|
minFilter: gl.LINEAR,
|
|
magFilter: gl.LINEAR,
|
|
});
|
|
|
|
const image = new Image();
|
|
image.src = imageSrc;
|
|
image.onload = () => {
|
|
texture.image = image;
|
|
texture.needsUpdate = true;
|
|
};
|
|
|
|
const program = new Program(gl, {
|
|
vertex,
|
|
fragment,
|
|
uniforms: {
|
|
tMap: { value: texture },
|
|
uResolution: { value: [1, 1] },
|
|
uImageResolution: { value: [1883, 855] },
|
|
uPointer: { value: [0.5, 0.5] },
|
|
uVelocity: { value: [0, 0] },
|
|
uStrength: { value: 0 },
|
|
},
|
|
transparent: true,
|
|
});
|
|
|
|
const mesh = new Mesh(gl, {
|
|
geometry: new Plane(gl, { width: 2, height: 2 }),
|
|
program,
|
|
});
|
|
|
|
let animationFrame = 0;
|
|
let width = 1;
|
|
let height = 1;
|
|
|
|
const resize = () => {
|
|
const rect = root.getBoundingClientRect();
|
|
width = Math.max(1, rect.width);
|
|
height = Math.max(1, rect.height);
|
|
renderer.setSize(width, height);
|
|
program.uniforms.uResolution.value = [width, height];
|
|
};
|
|
|
|
const onPointerMove = (event: PointerEvent) => {
|
|
const rect = root.getBoundingClientRect();
|
|
const isInside =
|
|
event.clientX >= rect.left &&
|
|
event.clientX <= rect.right &&
|
|
event.clientY >= rect.top &&
|
|
event.clientY <= rect.bottom;
|
|
|
|
if (!isInside) {
|
|
velocityRef.current = { x: 0, y: 0 };
|
|
return;
|
|
}
|
|
|
|
const nextPointer = {
|
|
x: (event.clientX - rect.left) / rect.width,
|
|
y: 1 - (event.clientY - rect.top) / rect.height,
|
|
};
|
|
const now = performance.now();
|
|
const elapsed = Math.max(16, now - lastMoveTimeRef.current);
|
|
lastMoveTimeRef.current = now;
|
|
velocityRef.current = {
|
|
x: ((nextPointer.x - pointerRef.current.x) / elapsed) * 16.67,
|
|
y: ((nextPointer.y - pointerRef.current.y) / elapsed) * 16.67,
|
|
};
|
|
pointerRef.current = nextPointer;
|
|
strengthRef.current = Math.min(1, strengthRef.current + 0.72);
|
|
};
|
|
|
|
const onPointerLeave = () => {
|
|
velocityRef.current = { x: 0, y: 0 };
|
|
};
|
|
|
|
const render = () => {
|
|
strengthRef.current *= 0.86;
|
|
velocityRef.current = {
|
|
x: velocityRef.current.x * 0.74,
|
|
y: velocityRef.current.y * 0.74,
|
|
};
|
|
|
|
program.uniforms.uPointer.value = [
|
|
pointerRef.current.x,
|
|
pointerRef.current.y,
|
|
];
|
|
program.uniforms.uVelocity.value = [
|
|
velocityRef.current.x,
|
|
velocityRef.current.y,
|
|
];
|
|
program.uniforms.uStrength.value = strengthRef.current;
|
|
renderer.render({ scene: mesh });
|
|
animationFrame = requestAnimationFrame(render);
|
|
};
|
|
|
|
const observer = new ResizeObserver(resize);
|
|
observer.observe(root);
|
|
window.addEventListener("pointermove", onPointerMove);
|
|
root.addEventListener("pointerleave", onPointerLeave);
|
|
|
|
resize();
|
|
animationFrame = requestAnimationFrame(render);
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animationFrame);
|
|
observer.disconnect();
|
|
window.removeEventListener("pointermove", onPointerMove);
|
|
root.removeEventListener("pointerleave", onPointerLeave);
|
|
gl.canvas.remove();
|
|
};
|
|
}, [imageSrc]);
|
|
|
|
return (
|
|
<div
|
|
ref={rootRef}
|
|
aria-hidden="true"
|
|
className="hero-ogl-scene absolute inset-[-18px]"
|
|
>
|
|
<div
|
|
className="absolute inset-0 bg-cover bg-center"
|
|
style={{ backgroundImage: `url(${imageSrc})` }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|