UNPKG

lightswind

Version:

A professionally designed animate react component library & templates market that brings together functionality, accessibility, and beautiful aesthetics for modern applications.

368 lines (328 loc) 15.1 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useEffect, useRef, useCallback } from 'react'; import { cn } from '../lib/utils'; import * as THREE from 'three'; const WoofyHoverImage = ({ src, alt = '', width = 'auto', height = 400, className, effectType = 'inversion', maskRadius = 0.35, turbulenceIntensity = 0.225, animationSpeed = 1.0, appearDuration = 0.4, disappearDuration = 0.3, effectIntensity = 0.5, invertMask = false, duotoneColor1 = '#3366cc', duotoneColor2 = '#e63333', onHover, onLeave, }) => { const containerRef = useRef(null); const sceneRef = useRef(null); const rendererRef = useRef(null); const uniformsRef = useRef(null); const animationIdRef = useRef(null); const isMouseInsideRef = useRef(false); const targetMouseRef = useRef(new THREE.Vector2(0.5, 0.5)); const lerpedMouseRef = useRef(new THREE.Vector2(0.5, 0.5)); const vertexShader = ` varying vec2 v_uv; void main() { v_uv = uv; gl_Position = vec4(position, 1.0); } `; const fragmentShader = ` precision highp float; uniform sampler2D u_texture; uniform vec2 u_mouse; uniform float u_time; uniform vec2 u_resolution; uniform float u_radius; uniform float u_speed; uniform float u_imageAspect; uniform float u_turbulenceIntensity; uniform int u_effectType; uniform vec3 u_effectColor1; uniform vec3 u_effectColor2; uniform float u_effectIntensity; uniform bool u_invertMask; varying vec2 v_uv; vec3 hash33(vec3 p) { p = fract(p * vec3(443.8975, 397.2973, 491.1871)); p += dot(p.zxy, p.yxz + 19.27); return fract(vec3(p.x * p.y, p.z * p.x, p.y * p.z)); } float simplex_noise(vec3 p) { const float K1 = 0.333333333; const float K2 = 0.166666667; vec3 i = floor(p + (p.x + p.y + p.z) * K1); vec3 d0 = p - (i - (i.x + i.y + i.z) * K2); vec3 e = step(vec3(0.0), d0 - d0.yzx); vec3 i1 = e * (1.0 - e.zxy); vec3 i2 = 1.0 - e.zxy * (1.0 - e); vec3 d1 = d0 - (i1 - K2); vec3 d2 = d0 - (i2 - K2 * 2.0); vec3 d3 = d0 - (1.0 - 3.0 * K2); vec3 x0 = d0; vec3 x1 = d1; vec3 x2 = d2; vec3 x3 = d3; vec4 h = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); vec4 n = h * h * h * h * vec4( dot(x0, hash33(i) * 2.0 - 1.0), dot(x1, hash33(i + i1) * 2.0 - 1.0), dot(x2, hash33(i + i2) * 2.0 - 1.0), dot(x3, hash33(i + 1.0) * 2.0 - 1.0) ); return 0.5 + 0.5 * 31.0 * dot(n, vec4(1.0)); } vec2 curl(vec2 p, float time) { const float epsilon = 0.001; float n1 = simplex_noise(vec3(p.x, p.y + epsilon, time)); float n2 = simplex_noise(vec3(p.x, p.y - epsilon, time)); float n3 = simplex_noise(vec3(p.x + epsilon, p.y, time)); float n4 = simplex_noise(vec3(p.x - epsilon, p.y, time)); float x = (n2 - n1) / (2.0 * epsilon); float y = (n4 - n3) / (2.0 * epsilon); return vec2(x, y); } float inkMarbling(vec2 p, float time, float intensity) { float result = 0.0; vec2 flow = curl(p * 1.5, time * 0.1) * intensity * 2.0; vec2 p1 = p + flow * 0.3; result += simplex_noise(vec3(p1 * 2.0, time * 0.15)) * 0.5; vec2 flow2 = curl(p * 3.0 + vec2(sin(time * 0.2), cos(time * 0.15)), time * 0.2) * intensity; vec2 p2 = p + flow2 * 0.2; result += simplex_noise(vec3(p2 * 4.0, time * 0.25)) * 0.3; vec2 flow3 = curl(p * 6.0 + vec2(cos(time * 0.3), sin(time * 0.25)), time * 0.3) * intensity * 0.5; vec2 p3 = p + flow3 * 0.1; result += simplex_noise(vec3(p3 * 8.0, time * 0.4)) * 0.2; float dist = length(p - vec2(0.5)); float angle = atan(p.y - 0.5, p.x - 0.5); float spiral = sin(dist * 15.0 - angle * 2.0 + time * 0.3) * 0.5 + 0.5; result = mix(result, spiral, 0.3); result = result * 0.5 + 0.5; return result; } vec3 applySepia(vec3 color) { float r = color.r * 0.393 + color.g * 0.769 + color.b * 0.189; float g = color.r * 0.349 + color.g * 0.686 + color.b * 0.168; float b = color.r * 0.272 + color.g * 0.534 + color.b * 0.131; return vec3(r, g, b); } vec3 applyDuotone(vec3 color, vec3 color1, vec3 color2) { float gray = dot(color, vec3(0.299, 0.587, 0.114)); return mix(color1, color2, gray); } vec3 applyPixelate(sampler2D tex, vec2 uv, float pixelSize) { float dx = pixelSize * (1.0 / u_resolution.x); float dy = pixelSize * (1.0 / u_resolution.y); vec2 pixelatedUV = vec2(dx * floor(uv.x / dx), dy * floor(uv.y / dy)); return texture2D(tex, pixelatedUV).rgb; } vec3 applyBlur(sampler2D tex, vec2 uv, float blurAmount) { float dx = blurAmount * (1.0 / u_resolution.x); float dy = blurAmount * (1.0 / u_resolution.y); vec3 sum = vec3(0.0); sum += texture2D(tex, uv + vec2(-dx, -dy)).rgb * 0.0625; sum += texture2D(tex, uv + vec2(0.0, -dy)).rgb * 0.125; sum += texture2D(tex, uv + vec2(dx, -dy)).rgb * 0.0625; sum += texture2D(tex, uv + vec2(-dx, 0.0)).rgb * 0.125; sum += texture2D(tex, uv).rgb * 0.25; sum += texture2D(tex, uv + vec2(dx, 0.0)).rgb * 0.125; sum += texture2D(tex, uv + vec2(-dx, dy)).rgb * 0.0625; sum += texture2D(tex, uv + vec2(0.0, dy)).rgb * 0.125; sum += texture2D(tex, uv + vec2(dx, dy)).rgb * 0.0625; return sum; } void main() { vec2 uv = v_uv; float screenAspect = u_resolution.x / u_resolution.y; float ratio = u_imageAspect / screenAspect; vec2 texCoord = vec2( mix(0.5 - 0.5 / ratio, 0.5 + 0.5 / ratio, uv.x), uv.y ); vec4 tex = texture2D(u_texture, texCoord); vec3 originalColor = tex.rgb; vec3 effectColor = originalColor; if (u_effectType == 1) { float gray = dot(originalColor, vec3(0.299, 0.587, 0.114)); effectColor = vec3(gray); } else if (u_effectType == 2) { effectColor = applySepia(originalColor); } else if (u_effectType == 3) { effectColor = applyDuotone(originalColor, u_effectColor1, u_effectColor2); } else if (u_effectType == 4) { effectColor = applyPixelate(u_texture, texCoord, u_effectIntensity * 20.0); } else if (u_effectType == 5) { effectColor = applyBlur(u_texture, texCoord, u_effectIntensity * 5.0); } vec2 correctedUV = uv; correctedUV.x *= screenAspect; vec2 correctedMouse = u_mouse; correctedMouse.x *= screenAspect; float dist = distance(correctedUV, correctedMouse); float marbleEffect = inkMarbling(uv * 2.0 + u_time * u_speed * 0.1, u_time, u_turbulenceIntensity * 2.0); float jaggedDist = dist + (marbleEffect - 0.5) * u_turbulenceIntensity * 2.0; float mask = u_radius > 0.001 ? step(jaggedDist, u_radius) : 0.0; vec3 invertedColor = vec3(0.0); if (u_effectType == 0) { float gray = dot(originalColor, vec3(0.299, 0.587, 0.114)); invertedColor = vec3(1.0 - gray); } else { invertedColor = originalColor; } vec3 finalColor; if (u_invertMask) { finalColor = mix(invertedColor, effectColor, mask); } else { finalColor = mix(effectColor, invertedColor, mask); } gl_FragColor = vec4(finalColor, 1.0); } `; const getEffectTypeValue = (type) => { switch (type) { case 'blackwhite': return 1; case 'sepia': return 2; case 'duotone': return 3; case 'pixelate': return 4; case 'blur': return 5; default: return 0; // inversion } }; const hexToRgb = (hex) => { return new THREE.Color(hex); }; const initializeEffect = useCallback(() => { if (!containerRef.current) return; const container = containerRef.current; const loader = new THREE.TextureLoader(); loader.load(src, (texture) => { const imageAspect = texture.image.width / texture.image.height; texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter; texture.anisotropy = 8; texture.generateMipmaps = false; const scene = new THREE.Scene(); sceneRef.current = scene; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const uniforms = { u_texture: { value: texture }, u_mouse: { value: new THREE.Vector2(0.5, 0.5) }, u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(containerWidth, containerHeight) }, u_radius: { value: 0.0 }, u_speed: { value: 0.75 }, u_imageAspect: { value: imageAspect }, u_turbulenceIntensity: { value: turbulenceIntensity }, u_effectType: { value: getEffectTypeValue(effectType) }, u_effectIntensity: { value: effectIntensity }, u_invertMask: { value: invertMask }, u_effectColor1: { value: hexToRgb(duotoneColor1) }, u_effectColor2: { value: hexToRgb(duotoneColor2) }, }; uniformsRef.current = uniforms; const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader, depthTest: false, depthWrite: false, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance", alpha: true, }); renderer.setPixelRatio(1); renderer.setSize(containerWidth, containerHeight); rendererRef.current = renderer; // Clear any existing canvas const existingCanvas = container.querySelector('canvas'); if (existingCanvas) { existingCanvas.remove(); } container.appendChild(renderer.domElement); renderer.domElement.style.position = 'absolute'; renderer.domElement.style.top = '0'; renderer.domElement.style.left = '0'; renderer.domElement.style.width = '100%'; renderer.domElement.style.height = '100%'; renderer.domElement.style.zIndex = '1'; // Animation loop const animate = () => { if (!uniformsRef.current || !rendererRef.current || !sceneRef.current) return; lerpedMouseRef.current.lerp(targetMouseRef.current, 0.1); uniformsRef.current.u_mouse.value.copy(lerpedMouseRef.current); if (isMouseInsideRef.current) { uniformsRef.current.u_time.value += 0.01 * animationSpeed; } rendererRef.current.render(sceneRef.current, camera); animationIdRef.current = requestAnimationFrame(animate); }; animate(); }); }, [src, effectType, maskRadius, turbulenceIntensity, animationSpeed, effectIntensity, invertMask, duotoneColor1, duotoneColor2]); const handleMouseMove = useCallback((e) => { if (!containerRef.current || !uniformsRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; if (inside) { targetMouseRef.current.x = (e.clientX - rect.left) / rect.width; targetMouseRef.current.y = 1.0 - (e.clientY - rect.top) / rect.height; if (!isMouseInsideRef.current) { isMouseInsideRef.current = true; onHover?.(); // Animate radius to target value const startRadius = uniformsRef.current.u_radius.value; const targetRadius = maskRadius; const startTime = Date.now(); const animateRadius = () => { const elapsed = (Date.now() - startTime) / 1000; const progress = Math.min(elapsed / appearDuration, 1); const easeProgress = 1 - Math.pow(1 - progress, 3); // ease-out cubic uniformsRef.current.u_radius.value = startRadius + (targetRadius - startRadius) * easeProgress; if (progress < 1) { requestAnimationFrame(animateRadius); } }; animateRadius(); } } else if (isMouseInsideRef.current) { isMouseInsideRef.current = false; onLeave?.(); // Animate radius to zero const startRadius = uniformsRef.current.u_radius.value; const startTime = Date.now(); const animateRadius = () => { const elapsed = (Date.now() - startTime) / 1000; const progress = Math.min(elapsed / disappearDuration, 1); const easeProgress = Math.pow(progress, 3); // ease-in cubic uniformsRef.current.u_radius.value = startRadius * (1 - easeProgress); if (progress < 1) { requestAnimationFrame(animateRadius); } }; animateRadius(); } }, [maskRadius, appearDuration, disappearDuration, onHover, onLeave]); useEffect(() => { initializeEffect(); document.addEventListener('mousemove', handleMouseMove, { passive: true }); return () => { document.removeEventListener('mousemove', handleMouseMove); if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); } if (rendererRef.current) { rendererRef.current.dispose(); } }; }, [initializeEffect, handleMouseMove]); return (_jsx("div", { ref: containerRef, className: cn(`relative overflow-hidden flex items-center justify-center`, className), style: { width, height }, children: _jsx("img", { src: src, alt: alt, className: "w-full h-full object-cover", style: { position: 'relative', zIndex: 0 } }) })); }; export default WoofyHoverImage;