I’m pretty new to vector graphics. I tried playing with WebGL for a bit but… WebGL is hard, man. This post explains it better than I can. Luckily, searching for npm packages tagged with “WebGL” yielded a very interesting library: regl. regl abstracts WebGL with a simple, intuitive API, somewhat inspired by React. Right up my alley.

This post is not an attempt to explain how OpenGL or shaders work. There are far better options out there. I’m merely documenting my own progress. In this post I’ll detail the two shaders required to render a texture.

Fragment Shader

precision mediump float;
uniform sampler2D texture;
varying vec2 uv;

void main() {
  gl_FragColor = texture2D(texture, uv);
}

This shader is straightforward. It reads from a texture and sets each pixel color to the same value as the texture color. precision determines how much precision the GPU uses when calculating floats. Other possible values are lowp and highp.

Vertex Shader

Rendering a texture was easy. Figuring out how to render it where I wanted was the hard part. I expected this to work:

precision mediump float;
attribute vec2 position;
varying vec2 uv;

void main () {
  uv = position;
  gl_Position = vec4(uv, 0, 1);
}

But this is the result:

Erhm, very cool looking in a glitch art kind of way, but not what I wanted.

The key point to understand here is that textures have no concept of “up”. The coordinates (0,0) reference the first pixel of the texture and the coordinates (1,1) reference the last pixel of the texture.

OpenGL on the other hand uses a different coordinate system. The domain of the “clip space” goes from -1 to 1 horizontally and from 1 to -1 vertically. This image can explain it better than I can:

As for the 3 glitched quadrants of the clip space, I suppose OpenGL takes pixels from the edges of the painted quadrant and uses them to fill the other 3.

The solution is some math, of course:

precision mediump float;
attribute vec2 position;
varying vec2 uv;

vec2 normalizeCoords(vec2 position) {
  // Center in clip space
  // Convert from 0->1 to -0.5->+0.5
  vec2 centered = position - 0.5;

  // Increase texture in size so that it covers the entire screen
  // Convert from -0.5->+0.5 to -1->1
  vec2 full = centered * 2.0;

  // Flip y
  return full * vec2(1, -1);
}

void main () {
  uv = position;
  gl_Position = vec4(normalizeCoords(position), 0, 1);
}

And there it is! Finally, in all its glory.