Skip to content

Instantly share code, notes, and snippets.

@seantai
Last active March 24, 2025 21:53
Show Gist options
  • Save seantai/8291191b3bbd77addafb456b73291580 to your computer and use it in GitHub Desktop.
Save seantai/8291191b3bbd77addafb456b73291580 to your computer and use it in GitHub Desktop.
LiquidRainbow.tsx
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