Last active
March 24, 2025 21:53
-
-
Save seantai/8291191b3bbd77addafb456b73291580 to your computer and use it in GitHub Desktop.
LiquidRainbow.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useFBO } from '@react-three/drei'; | |
| import { useFrame, useThree, type ThreeEvent } from '@react-three/fiber'; | |
| import { useCallback, useEffect, useMemo, useRef } from 'react'; | |
| import * as THREE from 'three'; | |
| export const LiquifyShader = () => { | |
| const gl = useThree((s) => s.gl); | |
| const size = useThree((s) => s.size); | |
| const viewport = useThree((s) => s.viewport); | |
| const mesh = useRef<THREE.Mesh>(null); | |
| const material = useRef<THREE.MeshBasicMaterial>(null); | |
| const mouseRef = useRef(new THREE.Vector4(0, 0, 0, 0)); | |
| const lastMouseRef = useRef(new THREE.Vector2(0, 0)); | |
| const isMouseDownRef = useRef(false); | |
| const currentTargetRef = useRef(0); | |
| const isInitializedRef = useRef(false); | |
| const fboOptions = { | |
| multisample: false, | |
| stencilBuffer: false, | |
| format: THREE.RGBAFormat, | |
| minFilter: THREE.LinearFilter, | |
| magFilter: THREE.LinearFilter | |
| }; | |
| const renderTargetA = useFBO(size.width, size.height, fboOptions); | |
| const renderTargetB = useFBO(size.width, size.height, fboOptions); | |
| const renderScene = useMemo(() => new THREE.Scene(), []); | |
| const renderCamera = useMemo( | |
| () => new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1), | |
| [] | |
| ); | |
| const renderMaterial = useMemo( | |
| () => | |
| new THREE.ShaderMaterial({ | |
| vertexShader: /* glsl */ ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| }`, | |
| fragmentShader: /* glsl */ ` | |
| uniform sampler2D uPreviousTexture; | |
| uniform vec2 uResolution; | |
| uniform vec4 uMouse; | |
| uniform float uTime; | |
| uniform float uAspect; | |
| varying vec2 vUv; | |
| // Aspect-ratio corrected UV calculation | |
| vec2 correctUV(vec2 uv) { | |
| // Center UVs | |
| vec2 centered = uv - 0.5; | |
| // Apply aspect ratio correction | |
| centered.x *= uAspect; | |
| // Return to 0-1 range | |
| return centered + 0.5; | |
| } | |
| // Uncorrect UVs for texture sampling | |
| vec2 uncorrectUV(vec2 uv) { | |
| vec2 centered = uv - 0.5; | |
| centered.x /= uAspect; | |
| return centered + 0.5; | |
| } | |
| // Calculate distance from point to line segment | |
| float lineDist(vec2 p, vec2 start, vec2 end, float width) { | |
| // Apply aspect ratio correction for distance calculation | |
| p = correctUV(p); | |
| start = correctUV(start); | |
| end = correctUV(end); | |
| vec2 dir = start - end; | |
| float lngth = length(dir); | |
| dir /= lngth; | |
| vec2 proj = max(0.0, min(lngth, dot((start - p), dir))) * dir; | |
| return length((start - p) - proj) - (width / 2.0); | |
| } | |
| // Spiral vortex effect with aspect ratio correction | |
| vec2 spiralVortex(vec2 uv, vec2 center, float strength, float spiralTightness) { | |
| // Convert to aspect-corrected space for calculations | |
| uv = correctUV(uv); | |
| center = correctUV(center); | |
| vec2 delta = uv - center; | |
| float dist = length(delta); | |
| float angle = atan(delta.y, delta.x) + strength * smoothstep(0.0, 0.5, 1.0 - dist) * 10.0; | |
| float spiral = spiralTightness * strength * smoothstep(0.0, 1.7, 1.0 - dist); | |
| float newDist = mix(dist, pow(dist, 1.0 + spiral), strength); | |
| // Return result in uncorrected space | |
| return uncorrectUV(center + vec2(cos(angle), sin(angle)) * newDist); | |
| } | |
| // Create rainbow colors based on input parameters | |
| vec3 rainbowColor(float offset, float scale, float timeScale) { | |
| return 0.5 + 0.5 * cos(timeScale * uTime + offset * scale + vec3(0.0, 2.0, 4.0)); | |
| } | |
| void main() { | |
| vec2 uv = vUv; | |
| vec2 mouse = uMouse.xy / uResolution.xy; | |
| vec2 prevMouse = uMouse.zw / uResolution.xy; | |
| vec2 distortedUv = uv; | |
| // Only apply effects when mouse is active (uMouse.z > 0.0) | |
| if (uMouse.z > 0.0) { | |
| // Calculate distance to mouse path | |
| float dist = lineDist(uv, mouse, prevMouse, 0.0); | |
| float strength = clamp(1.0 - (dist * 20.0), 0.0, 1.0) * 0.7; | |
| // Apply liquify effect based on mouse movement | |
| vec2 correctedUv = correctUV(uv); | |
| vec2 correctedMouse = correctUV(mouse); | |
| vec2 correctedPrevMouse = correctUV(prevMouse); | |
| vec2 moveDir = (correctedPrevMouse - correctedMouse) * strength; | |
| // Apply movement offset | |
| distortedUv = uncorrectUV(correctedUv + moveDir); | |
| // Calculate velocity for dynamic effects | |
| float velocity = length(mouse - prevMouse) * 10.0; | |
| float dynamicRadius = clamp(velocity, 0.05, 0.1); | |
| // Apply spiral vortex effect | |
| float vortexStrength = 0.1 * strength; | |
| vec2 vortexPoint = mix(prevMouse, mouse, 0.15); | |
| float distFactor = length(correctUV(uv) - correctUV(vortexPoint)) / dynamicRadius; | |
| float scaledStrength = vortexStrength * smoothstep(0.5, 0.0, distFactor); | |
| distortedUv = spiralVortex(distortedUv, vortexPoint, scaledStrength, 2.0 + sin(uTime) * 0.5); | |
| // Add ripple effect | |
| vec2 correctedPos = correctUV(uv); | |
| vec2 correctedMousePos = correctUV(mouse); | |
| float rippleDist = length(correctedPos - correctedMousePos); | |
| float rippleStrength = 0.02 * strength * sin(rippleDist * 40.0 - uTime * 2.0) * exp(-rippleDist * 6.0); | |
| vec2 rippleDir = normalize(correctedPos - correctedMousePos); | |
| distortedUv = uncorrectUV(correctUV(distortedUv) + rippleStrength * rippleDir); | |
| } | |
| // Apply color effects only after initialization | |
| if (uTime > 0.1) { | |
| vec4 baseColor = texture2D(uPreviousTexture, distortedUv); | |
| // Apply rainbow trail effect when mouse is moving | |
| if (uMouse.z > 0.0 && length(mouse - prevMouse) > 0.001) { | |
| vec2 velDir = normalize(mouse - prevMouse); | |
| float velMag = length(mouse - prevMouse) * 5.0; | |
| // Color effects based on movement | |
| float trailDist = lineDist(uv, mouse, prevMouse, 0.1); | |
| float trailMask = smoothstep(0.1, 0.0, trailDist) * velMag; | |
| // Add chromatic aberration | |
| vec2 rbOffset = velDir * trailMask * 0.03; | |
| vec4 chromaticColor = vec4( | |
| texture2D(uPreviousTexture, distortedUv + rbOffset).r, | |
| texture2D(uPreviousTexture, distortedUv).g, | |
| texture2D(uPreviousTexture, distortedUv - rbOffset).b, | |
| 1.0 | |
| ); | |
| // Create rainbow trail | |
| vec3 rainbow = rainbowColor(trailDist, 20.0, 3.0); | |
| // Blend effects | |
| baseColor.rgb = mix(baseColor.rgb, rainbow, trailMask * 0.7); | |
| baseColor = mix(baseColor, chromaticColor, trailMask * 0.5); | |
| } | |
| gl_FragColor = baseColor; | |
| } else { | |
| gl_FragColor = vec4(0.012, 0.013, 0.012, 1.0); | |
| } | |
| }`, | |
| uniforms: { | |
| uPreviousTexture: { value: null }, | |
| uResolution: { value: new THREE.Vector2(size.width, size.height) }, | |
| uMouse: { value: mouseRef.current }, | |
| uTime: { value: 0 }, | |
| uAspect: { value: size.width / size.height } | |
| }, | |
| toneMapped: false | |
| }), | |
| [size] | |
| ); | |
| useEffect(() => { | |
| if (!renderScene || !renderMaterial) return; | |
| const renderMesh = new THREE.Mesh( | |
| new THREE.PlaneGeometry(2, 2), | |
| renderMaterial | |
| ); | |
| renderScene.add(renderMesh); | |
| gl.setRenderTarget(renderTargetA); | |
| gl.render(renderScene, renderCamera); | |
| gl.setRenderTarget(renderTargetB); | |
| gl.render(renderScene, renderCamera); | |
| gl.setRenderTarget(null); | |
| isInitializedRef.current = true; | |
| return () => { | |
| renderScene.remove(renderMesh); | |
| }; | |
| }, [ | |
| renderScene, | |
| renderMaterial, | |
| gl, | |
| renderCamera, | |
| renderTargetA, | |
| renderTargetB | |
| ]); | |
| useFrame(({ clock }) => { | |
| if ( | |
| !isMouseDownRef.current || | |
| !isInitializedRef.current || | |
| !material.current | |
| ) { | |
| return; | |
| } | |
| const sourceBuffer = | |
| currentTargetRef.current === 0 ? renderTargetA : renderTargetB; | |
| const targetBuffer = | |
| currentTargetRef.current === 0 ? renderTargetB : renderTargetA; | |
| renderMaterial.uniforms.uTime.value = clock.elapsedTime; | |
| renderMaterial.uniforms.uMouse.value.copy(mouseRef.current); | |
| renderMaterial.uniforms.uPreviousTexture.value = sourceBuffer.texture; | |
| renderMaterial.uniforms.uAspect.value = size.width / size.height; | |
| gl.setRenderTarget(targetBuffer); | |
| gl.render(renderScene, renderCamera); | |
| gl.setRenderTarget(null); | |
| material.current.map = targetBuffer.texture; | |
| //flip target | |
| currentTargetRef.current = currentTargetRef.current === 0 ? 1 : 0; | |
| }); | |
| const handlePointerMove = useCallback( | |
| (e: ThreeEvent<PointerEvent>) => { | |
| if (isMouseDownRef.current && e.uv) { | |
| // Store last mouse position in z,w components | |
| mouseRef.current.z = lastMouseRef.current.x; | |
| mouseRef.current.w = lastMouseRef.current.y; | |
| // Update last mouse position | |
| lastMouseRef.current.x = mouseRef.current.x; | |
| lastMouseRef.current.y = mouseRef.current.y; | |
| // Update current mouse position with UV coordinates | |
| mouseRef.current.x = e.uv.x * size.width; | |
| mouseRef.current.y = e.uv.y * size.height; | |
| } | |
| }, | |
| [size] | |
| ); | |
| const handlePointerDown = useCallback( | |
| (e: ThreeEvent<PointerEvent>) => { | |
| if (!e.uv) return; | |
| isMouseDownRef.current = true; | |
| // Initialize last position to current position | |
| const x = e.uv.x * size.width; | |
| const y = e.uv.y * size.height; | |
| lastMouseRef.current.x = x; | |
| lastMouseRef.current.y = y; | |
| // Set current and previous mouse positions | |
| mouseRef.current.x = x; | |
| mouseRef.current.y = y; | |
| mouseRef.current.z = x; | |
| mouseRef.current.w = y; | |
| if (e.target && 'setPointerCapture' in e.target) { | |
| (e.target as Element).setPointerCapture(e.pointerId); | |
| } | |
| }, | |
| [size] | |
| ); | |
| const handlePointerUp = useCallback((e: ThreeEvent<PointerEvent>) => { | |
| isMouseDownRef.current = false; | |
| // Reset z component to indicate mouse is up | |
| mouseRef.current.z = 0; | |
| mouseRef.current.w = 0; | |
| if (e.target && 'releasePointerCapture' in e.target) { | |
| (e.target as Element).releasePointerCapture(e.pointerId); | |
| } | |
| }, []); | |
| return ( | |
| <mesh | |
| ref={mesh} | |
| scale={[viewport.width, viewport.height, 1]} | |
| onPointerDown={handlePointerDown} | |
| onPointerMove={handlePointerMove} | |
| onPointerUp={handlePointerUp}> | |
| <planeGeometry args={[2, 2]} /> | |
| <meshBasicMaterial | |
| ref={material} | |
| map={renderTargetA.texture} | |
| toneMapped={false} | |
| /> | |
| </mesh> | |
| ); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment