live - 60fps 1920 x 1080
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Liquid Distortion Shader</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="glcanvas"></canvas>
<!-- Vertex Shader -->
<script id="v-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
varying vec2 v_uv;
void main() {
v_uv = a_position * 0.5 + 0.5;
v_uv.y = 1.0 - v_uv.y;
gl_Position = vec4(a_position, 0.0, 1.0);
}
</script>
<!-- Fragment Shader -->
<script id="f-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec2 v_uv;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
// Configuration colors (passed as uniforms in a real app, hardcoded here for simplicity)
const vec3 color1 = vec3(0.0, 0.4, 1.0); // #0066ff
const vec3 color2 = vec3(1.0, 0.0, 0.5); // #ff0080
// Inigo Quilez Simplex Noise 2D
vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
float snoise(vec2 v){
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m;
m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
float aspect = u_resolution.x / u_resolution.y;
st.x *= aspect;
vec2 mouse = u_mouse / u_resolution.xy;
mouse.x *= aspect;
// Liquid distortion calculation
float noiseTime = u_time * 0.2;
vec2 noiseCoord = st * 3.0;
float n = snoise(noiseCoord + vec2(noiseTime));
float n2 = snoise(noiseCoord * 2.0 - vec2(noiseTime * 1.5));
vec2 distortedUV = st + vec2(n, n2) * 0.1;
// Mouse interaction
float distToMouse = distance(st, mouse);
float mouseInfluence = smoothstep(0.4, 0.0, distToMouse);
distortedUV += (st - mouse) * mouseInfluence * 0.2;
// Color mixing
float mixer = snoise(distortedUV * 2.0 + u_time * 0.1) * 0.5 + 0.5;
vec3 finalColor = mix(color1, color2, mixer);
// Add some bright specular highlights
float highlight = smoothstep(0.8, 1.0, snoise(distortedUV * 5.0 - u_time));
finalColor += vec3(highlight * 0.5);
gl_FragColor = vec4(finalColor, 1.0);
}
</script>
<script>
const canvas = document.getElementById('glcanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
console.error('WebGL not supported');
document.body.innerHTML = '<div style="color:white;text-align:center;padding:50px;">WebGL is required for this animation.</div>';
} else {
// Compile shader
function compileShader(type, id) {
const source = document.getElementById(id).text;
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Error compiling shader:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = compileShader(gl.VERTEX_SHADER, 'v-shader');
const fragmentShader = compileShader(gl.FRAGMENT_SHADER, 'f-shader');
// Create program
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Error linking program:', gl.getProgramInfoLog(program));
}
gl.useProgram(program);
// Set up buffer
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
-1.0, 1.0,
1.0, -1.0,
1.0, 1.0
]), gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// Uniforms
const timeLocation = gl.getUniformLocation(program, 'u_time');
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
const mouseLocation = gl.getUniformLocation(program, 'u_mouse');
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = window.innerHeight - e.clientY; // Flip Y for WebGL
});
window.addEventListener('touchmove', (e) => {
mouseX = e.touches[0].clientX;
mouseY = window.innerHeight - e.touches[0].clientY;
});
resize();
// Render loop
function render(time) {
gl.uniform1f(timeLocation, time * 0.001);
gl.uniform2f(mouseLocation, mouseX, mouseY);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
</script>
</body>
</html> About this animation
A WebGL displacement shader creating a liquid, glassy refraction effect over neon gradients.
Under the hood
Simplex Noise GLSL Shading
Driven purely by the GPU rather than the CPU, this WebGL fragment shader utilizes
snoise (Simplex Noise) mathematically injected into the texture coordinates. The noise matrix continuously warps the perceived space between two neon uniform colors, outputting incredibly smooth, liquid-like refraction. Related