Pixel Art Editor using WebGL

Foreword

I was looking for a fun project to do and came up with this editor (see in action):

ValeriaVG
/
pixel-vg

WebGL Pixel Editor

Pixel VG – WebGL Pi…


This content originally appeared on DEV Community and was authored by Valeria

Foreword

I was looking for a fun project to do and came up with this editor (see in action):

GitHub logo ValeriaVG / pixel-vg

WebGL Pixel Editor

Pixel VG - WebGL Pixel Editor

Pixel (V)ector (G)raphics Easy to use point & click pixel-art editor.

Development

Install dependencies:

npm ci

Run in dev mode:

npm run dev

Preview build:

npm run preview

Build static version:

npm run build



As you can see it looks really simple and there is a lot of improvements that can be done of varying difficulty: from styling to dev-ops.

The project is tagged with #hacktoberfest and I'll do my best to review pull requests in a timely manner. Just grab one of the existing 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, 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:

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:

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:

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:

// 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:

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:

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:

precision mediump float;
varying vec4 v_color;

void main() {
  gl_FragColor = v_color;
}

With a couple of math functions we can turn this array:

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:

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

<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.clientX - canvasRect.left) / blockSize) * PIXEL_RATIO);
    const y = Math.floor(((e.clientY - canvasRect.top) / blockSize) * PIXEL_RATIO);
    recordPoint(x, y);
    render();
};

$: canvasRect = canvas?.getBoundingClientRect();
</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 editor!


This content originally appeared on DEV Community and was authored by Valeria


Print Share Comment Cite Upload Translate Updates
APA

Valeria | Sciencx (2021-10-03T12:19:24+00:00) Pixel Art Editor using WebGL. Retrieved from https://www.scien.cx/2021/10/03/pixel-art-editor-using-webgl/

MLA
" » Pixel Art Editor using WebGL." Valeria | Sciencx - Sunday October 3, 2021, https://www.scien.cx/2021/10/03/pixel-art-editor-using-webgl/
HARVARD
Valeria | Sciencx Sunday October 3, 2021 » Pixel Art Editor using WebGL., viewed ,<https://www.scien.cx/2021/10/03/pixel-art-editor-using-webgl/>
VANCOUVER
Valeria | Sciencx - » Pixel Art Editor using WebGL. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/10/03/pixel-art-editor-using-webgl/
CHICAGO
" » Pixel Art Editor using WebGL." Valeria | Sciencx - Accessed . https://www.scien.cx/2021/10/03/pixel-art-editor-using-webgl/
IEEE
" » Pixel Art Editor using WebGL." Valeria | Sciencx [Online]. Available: https://www.scien.cx/2021/10/03/pixel-art-editor-using-webgl/. [Accessed: ]
rf:citation
» Pixel Art Editor using WebGL | Valeria | Sciencx | https://www.scien.cx/2021/10/03/pixel-art-editor-using-webgl/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.