"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(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 (