Skip to content

Instantly share code, notes, and snippets.

@aza
Created September 3, 2025 16:17
Show Gist options
  • Select an option

  • Save aza/d008e367e3159132355ebc92063894f2 to your computer and use it in GitHub Desktop.

Select an option

Save aza/d008e367e3159132355ebc92063894f2 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL Ripple Filter</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Inter', sans-serif;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
.info {
font-family: monospace;
color: #ccc;
background: rgba(0,0,0,0.5);
padding: 0.5em 1em;
border-radius: 8px;
}
</style>
</head>
<body class="bg-gray-900 flex items-center justify-center h-screen">
<!-- This canvas is where the WebGL magic happens -->
<canvas id="gl-canvas"></canvas>
<!-- Error message for browsers without WebGL support -->
<div id="webgl-error" class="hidden absolute inset-0 bg-red-900/80 text-white flex items-center justify-center p-8 text-center">
<p class="text-lg">Sorry, WebGL is not supported or is disabled in your browser. This experience requires WebGL to run.</p>
</div>
<div class="absolute bottom-4 left-4 info">
WebGL Ripple Filter
</div>
<!-- Vertex Shader: Positions vertices -->
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
</script>
<!-- Fragment Shader: Colors pixels and creates the ripple effect -->
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D u_texture;
uniform float u_time;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec2 uv = v_texCoord;
if (uv.y > 0.5) {
// 'progress' goes from 0.0 at the halfway mark to 1.0 at the bottom.
float progress = (uv.y - 0.5) * 2.0;
// The number of waves grows as we go down the screen.
float frequency = 25.0 + (600.0 * progress / progress);
// The displacement grows from 0 at the center to max at the bottom.
// The scale of the displacement is now 2x bigger (0.01 -> 0.02).
float amplitude = 0.055 * progress * progress;
// The animation speed is now 2x faster (0.6 -> 1.2).
float distortion = sin(uv.y * frequency + u_time * 1.6) * amplitude;
uv.x += distortion;
}
gl_FragColor = texture2D(u_texture, uv);
}
</script>
<script type="module">
// IMPORTANT: Replace this with the URL of your image.
const imageUrl = 'https://placehold.co/2048x1024/FFFFFF/000000?text=ESP&font=inter';
function main() {
const canvas = document.getElementById('gl-canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
document.getElementById('gl-canvas').classList.add('hidden');
document.getElementById('webgl-error').classList.remove('hidden');
console.error('WebGL is not supported by your browser.');
return;
}
// 1. Setup GLSL program
const vsSource = document.getElementById('vertex-shader').text;
const fsSource = document.getElementById('fragment-shader').text;
const program = createProgram(gl, vsSource, fsSource);
// 2. Look up attribute and uniform locations
const positionLocation = gl.getAttribLocation(program, 'a_position');
const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
const textureLocation = gl.getUniformLocation(program, 'u_texture');
const timeLocation = gl.getUniformLocation(program, 'u_time');
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
// 3. Create buffers for a quad that fills the canvas
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,1, 1,1, 0,0, 0,0, 1,1, 1,0]), gl.STATIC_DRAW);
// 4. Load the image and create a texture
const texture = createTextureFromURL(gl, imageUrl);
// 5. Render loop
function render(time) {
resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.1, 0.1, 0.12, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texCoordLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.uniform1f(timeLocation, time * 0.001);
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
gl.uniform1i(textureLocation, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function createTextureFromURL(gl, url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
const image = new Image();
image.crossOrigin = "anonymous";
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
};
image.onerror = () => {
console.error("Failed to load image texture from URL:", url);
}
image.src = url;
return texture;
}
// --- WebGL Helper Functions ---
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vsSource, fsSource) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(program));
return null;
}
return program;
}
function resizeCanvasToDisplaySize(canvas) {
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
return true;
}
return false;
}
main();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment