Pixel Art Editor using WebGL
Tags: beginners, svelte, webgl, hacktoberfest
Published: 2021-10-03 12:19:24

I was looking for a fun project to do and came up with this [editor](https://pixel-vg-6ijid.ondigitalocean.app/).

By the way, the project is tagged with *#hacktoberfest*, feel free to grab one of the existing [issues](https://github.com/ValeriaVG/pixel-vg/issues) or create a PR without one.

## How the editor works

WebGL is a technology that allows HTMLCanvas to talk to GPUs directly via a language called GLSL. To put it simply, you provide WebGL context with coordinates, colours and two functions:
- *vertex shader*, that tells **where** each vertex should be positioned
- *fragment shader*, that tells **what colour** should every vertex or point on the screen have

If you're looking for an in-depth introduction to WebGL, check [this article by Maxime Euzière](https://xem.github.io/articles/webgl-guide.html), it's very good.

The editor I wrote uses two sets of shaders: 
- first one draws the pixel points on the screen 
- and the other draws a thin grid above it.

### Drawing grid with WebGL

Vertex shader for grid simply sets position to the exact value it's fed with:

```glsl
attribute vec4 position;
void main() {
  gl_Position = position;
}
```

And the fragment shader discards all the pixels, apart from a thin interval around coordinates that are dividable by cell size:

```glsl
precision mediump float;
uniform float size;

void main() {
  if(
   mod(gl_FragCoord.x,size)<1.0 ||
   mod(gl_FragCoord.y,size)<1.0
  ){
    gl_FragColor = vec4(0.0, 0.0, 0.0, 0.8);
  }else {discard;}                      
}
```

These shaders are compiled like this:
```ts
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
// Compile vertex shader
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vertexShader);
gl.compileShader(vs);

// Compile fragment shader
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fragmentShader);
gl.compileShader(fs);

// Create and launch the WebGL program
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
```

And can be used like this:
```ts
// Clear the canvas
gl.clear(gl.COLOR_BUFFER_BIT);
// Activate grid shaders
gl.useProgram(program);

// Set size value
const size = gl.getUniformLocation(program, 'size');
gl.uniform1f(size, 32); // Cell Size

// Four vertices represent corners of the canvas
// Each row is x,y,z coordinate
// -1,-1 is left bottom, z is always zero, since we draw in 2d
const vertices = new Float32Array([
 1.0, 1.0, 0.0, 
-1.0, 1.0, 0.0, 
 1.0, -1.0, 0.0, 
-1.0, -1.0, 0.0
]);

// Attach vertices to a buffer
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// Set position to point to buffer
const position = gl.getAttribLocation(program, 'position');

gl.vertexAttribPointer(
	position, // target
	3, // x,y,z
	gl.FLOAT, // type
	false, // normalize
	0, // buffer offset
	0 // buffer offset
);

gl.enableVertexAttribArray(position);

// Finally draw our 4 vertices
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
```

Here's the result:
{% codepen https://codepen.io/valeriavg/pen/bGpYGqZ %}

## Drawing pixels on the grid

To draw square pixels we will simply set vertices to the dots we want to draw and set their size to `32`- to match the grid.

Fragment shaders do not have access to the buffer itself, so in order to properly colour the points, we need to pass this colour information from the vertex shader:

```glsl
attribute vec4 position;
attribute vec4 color;
varying vec4 v_color;
uniform float size;

void main() {
  gl_Position = position;
  v_color = color;
  gl_PointSize = size;
}
```

And the fragment shader will look really simple now:
```glsl
precision mediump float;
varying vec4 v_color;

void main() {
  gl_FragColor = v_color;
}
```

With a couple of math functions we can turn this array:
```js
const pixels = [
  [0, 0, "#ff0000"],
  [1, 1, "#ffaa00"],
  [2, 2, "#ffff00"],
  [3, 3, "#00ff00"],
  [4, 4, "#00ffaa"],
  [5, 5, "#00ffff"],
  [6, 6, "#0000ff"],
  [7, 7, "#ff00aa"],
  [8, 8, "#ff00ff"]
];
```

Into this:
{% codepen https://codepen.io/valeriavg/pen/XWgwmpj %}

And the rest of the editor is svelte's magic:

```html
<script lang="ts">
import { onMount } from 'svelte';
omMount(()=>{
  /** compile shaders **/
})
const render = ()=>{
 /** render stuff **/
}

const PIXEL_RATIO = window.devicePixelRatio;
let canvas: HTMLCanvasElement;
let gl: WebGLRenderingContext;

export let blockSize = 32;
export let size: number = 16;
// [x,y,color]
export let pixels: Array<[number, number, string]> = [];
export let color: string = '#ff0000';

const recordPoint = (x: number, y: number) => {
	pixels = pixels.filter(([px, py]) => x !== px || y !== py);
	if (color) {
	// Draw
	pixels.push([x, y, color]);
  }
};

const onClick = (e) => {
	const x = Math.floor(e.offsetX / blockSize * PIXEL_RATIO);
	const y = Math.floor(e.offsetY / blockSize * PIXEL_RATIO);
	recordPoint(x, y);
	render();
};

</script>

<canvas
	bind:this={canvas}
	width={size * blockSize}
	height={size * blockSize}
	style={`width:${(size * blockSize) / PIXEL_RATIO}px; height:${
		(size * blockSize) / PIXEL_RATIO
	}px;`}
	on:click={onClick}
/>

```

Which boils down to this:
- locate clicked cell coordinates
- add new pixel with selected colour and coordinates

## Why WebGL

Drawing static pictures with 2D canvas methods like `fillRect` and `lineTo` is quite easy, but is you need to redraw contents often it quickly becomes visibly slow.

And besides, once you get a grip of WebGL it's not much harder to write shaders than operate old-school canvas.

## Afterword

Thank you for your time and I hope you found this article useful.

And don't be a stranger, join the development of the [pixel-vg](https://github.com/ValeriaVG/pixel-vg) editor!