Why did I spend so much time on this Website?
I'm not a web dev. What am I doing?
Web development
It may be obvious to you by looking at this website (especially on mobile) that I am not a web dev. I fully considered using Squarespace and being done with it, but something about that just felt so wrong. I love customization, and I didn’t want to feel limited. I know some web developers, and when I asked what they like to use to make websites, the answer is inevitably “React”.
I have nothing against React, but I’m not here to become a web developer. I want static pages. I want pictures. I want a blog. I want it to look presentable. That’s it. I’m a big believer in using the right tool for the right job. My focus is, after all, graphics programming. I hope that’s obvious.
I googled something like “React, but for web dev idiots” and found a reddit blog where someone said to use Astro. They were so right, I had a website that looked incredibly functional after about 3 hours.
WebGL: The Setup
So that only took 3 hours right? Then I was done? Not exactly.
Im sure every graphics engineer is aware of “The Book of Shaders” or shadertoy. Instead of making something cool in shader toy and linking it, I thought “What if I integrate something like that myself?” I could choose the easy path, but I already did that once for this website, and this is all about graphics programming.
So that was that. I decided to use WebGL. The first thing you need is a canvas to render to. I made a ShaderView.tsx file, which does a lot of good stuff, but it creates a ShaderCanvas function that returns something like:
return (
<canvas
ref={canvasRef}
className={className} />
);
Now I need something to manage the lifecycle of my effect. Here’s what I need:
- A WebGL context
- Vertex Shader
- Fragment Shader
- A WebGL Program
- A full screen quad
- The attributes and buffers to bind it all together
- A time uniform to make some really cool animations
- A render function
- Error checking. Always spend an hour to save 3 hours later
Perfect. Let’s make a useEffect function. We’ll start with a context.
const gl = canvas.getContext('webgl', { alpha: false }) || canvas.getContext('experimental-webgl') as WebGLRenderingContext;
I’ll create my shader functions from a source string, so I can reuse this and make multiple effects:
function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
and make those shaders into a program:
function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram | null {
const program = gl.createProgram();
if (!program) return null;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
//use it in the useEffect function
const program = createProgram(gl, vertexShader, fragmentShader);
if (!program) {
console.error('Failed to create shader program');
return;
}
programRef.current = program;
gl.useProgram(program);
Now we need that full screen quad I mentioned. In WebGL, the UVs go from -1 to +1. In graphics, we draw triangles, and a quad is 2 triangle, meaning we need 6 points. So it looks like:
// Setup default geometry (full-screen quad)
const positions = new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]);
This ensures that the whole view is taken up by this flat panel. Let’s get the buffers set up and bind it to our data.
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
// Get this uniform so we can updte it later
const timeUniformLocation = gl.getUniformLocation(program, 'u_time');
We’re almost done with the setup, I promise. We just need a render function to update everything
//calculate elapsed time
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
if (timeUniformLocation) {
gl.uniform1f(timeUniformLocation, elapsedTime);
}
gl.drawArrays(gl.TRIANGLES, 0, 6);
I can get away with the hardcoded 6 because I’m not planning to render anything else. The shader code is what’s going to change.
Note: This isn’t a comprehensive setup, I left a couple parts out, including some custom callbacks. Let me know if you’d like the full code.
WebGL: The fun part
Phew. Now that setup is out of the way, time to have some fun with shaders!
Vertex shaders are the less sexy of the shaders I’m using, so today, we’ll just be talking about fragment shaders. The full code for the fragment shaders are on my homepage!
How do you find inspiration for the kinds of shaders and effects you want to make? Mine tend to be really seasonal and tied to nature. I wrote these shaders over the summer (about 5 months ago!) and I tend to really enjoy stargazing in the warmer months. Hence, 2 of the 3 WebGL shaders on my website are space themed. I knew I wanted a particle shader, some kind of loud visually interesting animation, and a more complex space scene.
Starfield
I love this. The color, the background. Everyone recognizes the Star Wars crawl starfield. I wanted something with that vibe, but more alive.

I considered making just a white starfield and having my name be in blue like these credits. But I knew I wanted to switch out effects, and that was a web dev problem I didn’t want to solve. I opted for blue animated particles.
Here’s the color I settled on.
vec3 particleColor = vec3(0.3, 0.6, 0.9);
The process is pretty simple. You divide the uvs into a grid, and then randomize the particle’s position.
vec2 particleGrid = floor(particleUV);
vec2 particleFract = fract(particleUV);
// Create particle at random position in each grid cell
vec2 particlePos = vec2(random(particleGrid), random(particleGrid + vec2(1.0, 1.0)));
Just animate the particle by using a random offset based on time!
// Animate particle position
float particleTime = u_time * 0.5 + random(particleGrid) * 6.28;
particlePos += vec2(sin(particleTime), cos(particleTime * 1.3)) * 0.2;
Those are the fundamentals of a basic starfield! You can view the code on my homepage. I also animated the alpha, so they fade in and out.
Go Retro
I wanted a shader that was loud, fun, animated, with a glowing effect. How about a neon retro feel?
Let’s talk palette functions.
I decided to pick 4 colors, thank you coolors. I picked some dark indigo for a background and bright colors for contrast. I made a palette function animated with t and did the following to make a nice gradient:
vec3 palette(float t) {
vec3 a = vec3(0.08, 0.05, 0.12); // Dark indigo
vec3 b = vec3(0.15, 0.12, 0.25); // Variation
vec3 c = vec3(0.8, 0.9, 1.0);
vec3 d = vec3(0.2, 0.3, 0.6);
return a + b * cos(6.28318 * (c * t + d));
}
This gives a scrolling rainbow of neon colors. The magic is the cosine. It naturally produces smooth looping gradients.
Here’s the guts of the operation.
for (float i = 0.0; i < 2.0; i++) {
uv = fract(uv * 2.0) - 0.5;
//...
}
This is UV distortion. If you keep zooming the UVs in, wrapping them, and recenterring them, it creates this repeating, mirrored motion that feels very retro. It looks like a kaleidoscope.
For the neon glow, later in that for loop, I do:
d = cos(d * 6.0 + u_time * 0.8) / 12.0;
d = abs(d);
d = pow(0.005 / d, 0.8);
The cosine wave and the power falloff make the bright areas really bright, and let everything else melt into a soft glow.
And here it is, a neon kaleidoscope background!

To be continued?
I have 1 more shader on my homepage that I’d like to cover, but I think it deserves it’s own post. Stay tuned for that!
Website: The final polish
Why force people to look at my code on a different website, when I can put it in front of your eyes on my homepage? The tabs on my homepage are actually inspired by VS Code. Wiring that up with the toggle button and switching between views was my least favorite part of this whole project, which reaffirmed my decision to pursue graphics over web dev.
This was seriously such a fun project, and honestly, building it taught me more about WebGL than any tutorial ever has. Hope y’all enjoyed!