simple utility to render a .glsl fragment shader

10/4/2026

ts

shaders


if you’ve ever wanted to render a .glsl fragment shader in browser without heavyweight graphics libraries, i got you. just use setupWebGL(canvasElement, shader). position and size your canvas using css. to cleanup/destroy call the function returned.

import { setupWebGL } from "./webgl";

const shader = `#version 300 es

precision mediump float;

uniform float u_time;
uniform vec2 u_resolution;

out vec4 fragColor;

void main() {
    vec2 uv = gl_FragCoord.xy / u_resolution;
	vec2 centered = (uv - 0.5) * 2.0;

	float angle = atan(centered.y, centered.x) + u_time;
	float normalized = fract((angle + PI) / (2.0 * PI));

    fragColor = vec4(pow(normalized, -1.0) * vec3(uv, 1.0), 1.0);
}`;

setupWebGL(document.querySelector<HTMLCanvasElement>(".gl")!, shader);
example usage

const quadVertexShaderGlsl = `#version 300 es

in vec4 position;

void main() {
    gl_Position = position;
}`;

const quadVertices = new Float32Array(
	[
		[-1, 3],
		[-1, -1],
		[3, -1],
	].flat(),
);

export function setupWebGL(
	canvasElement: HTMLCanvasElement,
	quadFragmentShaderGlsl: string,
): (() => void) | void {
	const gl = canvasElement.getContext("webgl2");
	if (!gl) return;

	let dpr = window.devicePixelRatio;
	let width = Math.floor(canvasElement.clientWidth * dpr);
	let height = Math.floor(canvasElement.clientHeight * dpr);

	const quadVertexShader = createShader(
		gl,
		gl.VERTEX_SHADER,
		quadVertexShaderGlsl,
	);
	const quadFragmentShader = createShader(
		gl,
		gl.FRAGMENT_SHADER,
		quadFragmentShaderGlsl,
	);

	const bgProgram = createProgram(gl, quadVertexShader, quadFragmentShader);

	gl.deleteShader(quadVertexShader);
	gl.deleteShader(quadFragmentShader);

	const bgLocations = {
		position: gl.getAttribLocation(bgProgram, "position"),
		time: gl.getUniformLocation(bgProgram, "u_time"),
		resolution: gl.getUniformLocation(bgProgram, "u_resolution"),
	};

	const positionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW);

	const quadVao = gl.createVertexArray();
	gl.bindVertexArray(quadVao);
	gl.enableVertexAttribArray(bgLocations.position);
	gl.vertexAttribPointer(bgLocations.position, 2, gl.FLOAT, false, 0, 0);

	let raf: number;
	let startedAt = performance.now();
	let resized = true;

	const canvasResizeObserver = new ResizeObserver(() => (resized = true));
	canvasResizeObserver.observe(canvasElement);

	const frame = (time: number) => {
		if (resized) {
			dpr = window.devicePixelRatio;
			width = Math.floor(canvasElement.clientWidth * dpr);
			height = Math.floor(canvasElement.clientHeight * dpr);

			canvasElement.width = width;
			canvasElement.height = height;

			gl.viewport(0, 0, width, height);

			resized = false;
		}

		gl.useProgram(bgProgram);
		gl.bindVertexArray(quadVao);

		gl.uniform1f(bgLocations.time, (time - startedAt) / 1000);
		gl.uniform2f(bgLocations.resolution, width, height);

		gl.clearColor(0, 0, 0, 0);
		gl.clear(gl.COLOR_BUFFER_BIT);
		gl.drawArrays(gl.TRIANGLES, 0, Math.floor(quadVertices.length / 2));

		raf = requestAnimationFrame(frame);
	};

	raf = requestAnimationFrame(frame);

	return () => {
		cancelAnimationFrame(raf);
		canvasResizeObserver.disconnect();
		const loseContextExt = gl.getExtension("WEBGL_lose_context");
		if (loseContextExt) loseContextExt.loseContext();
	};
}

function createShader(
	gl: WebGL2RenderingContext,
	type:
		| WebGLRenderingContextBase["VERTEX_SHADER"]
		| WebGLRenderingContextBase["FRAGMENT_SHADER"],
	source: string,
): WebGLShader {
	const shader = gl.createShader(type);
	if (!shader) throw new Error("gl.createShader");

	gl.shaderSource(shader, source);
	gl.compileShader(shader);

	if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;

	const log = gl.getShaderInfoLog(shader);
	gl.deleteShader(shader);

	throw new Error(log ?? "?");
}

function createProgram(
	gl: WebGL2RenderingContext,
	vertexShader: WebGLShader,
	fragmentShader: WebGLShader,
): WebGLProgram {
	const program = gl.createProgram();

	gl.attachShader(program, vertexShader);
	gl.attachShader(program, fragmentShader);
	gl.linkProgram(program);

	if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;

	const log = gl.getProgramInfoLog(program);
	gl.deleteProgram(program);

	throw new Error(log ?? "?");
}
webgl.ts

credits:


have a question/comment? want to say hi?