WebGL Colour Palette Preview
I decided to rebuild a project I made a few years ago in Codepen that enabled you to build up a colour palette, and view it on a HSL gradient. The idea is that you could visualise colours that are relatively unused, or add colours to fill in gaps.
Here it is, but using the Pico8 palette, one of my favourites.
This first implementation used a generator function, as generating the pixel values took too long to do it all in one go
// Generator
function* genPixel(mapToPalette, saturation) {
// Hue
for (let h = 0; h < 360; h++) {
// Brightness
for (let b = 0; b < 200; b++) {
// etc
yield { r: rgb[0], g: rgb[1], b: rgb[2], pos: [h, b] };
This generator function would basically act like an iterator, where I'd call .next()
until I'd ran out of time, and then I'd draw some more on the next frame.
Re-implementing with WebGL
The idea with this new project was to use WebGL so it could be drawn instantly
My approach was to fraw a full-screen plane for each colour in the pallete. For every pixel I would set the depth based on how far (in 3d RGB space) away it was from the HSL gradient. The colour that was closer would appear in front.
Here's the new version and here's the code
I had to specify version 330 es
for the ability to output a depth value. I'll hide the implementation details of the colour function to show the imoprtant bits
#version 300 es
precision highp float;
out vec4 o_color;
uniform vec4 uCol;
float hue2rgb(float p, float q, float t) {
// ...
}
vec3 hslToRgb(float h, float s, float l) {
// ...
}
float distSq(vec3 a, vec3 b){
// ...
}
void main(void)
{
float hue = gl_FragCoord.x / 360.0; // hue is 0-360
float saturation = uCol.a; // Saturation is passed in via alpha channel
float lightness = 1.0 - gl_FragCoord.y / 100.0; // lightness is 0-100, flipped
gl_FragDepth = distSq(uCol.rgb, hslToRgb(hue, saturation, lightness)); // distance squared is faster, and sufficient for sorting
o_color = vec4(uCol.rgb,1.0);
}
Here's a one-dimensional representation of what's going on. The closest colour to the camera (y=0) is what gets shown in the result, even though we calculate the depth (RGB distance) of each pixel. This lets us loop through as many colours as we like without having a loop inside the shader.
UI controls with React and MUI
I decided to use React and Material UI for the user interface, which unfortunately added megabytes to the otherwise tiny codebase (but the internet is fast these days). The trickier part for this project was to interface with the canvas and realtime rendering.
My solution was rather dirty though, basically a global instance that managed rendering that the UI components would be able to trigger, plus some hooks
React.useEffect(() => {
GL_GENERATOR.updateColours(colours);
});
Another tricky part was trying to trigger the native colour picker, that I might replace later. The trick was to use the css display:hidden
and call the input element's click()
function to open it.
const colourPicker = React.useRef(null);
// ...
function showThePicker(){
if(colourPicker.current){
colourPicker.current.click();
}
}
// ...
return <input type="color" id="color-picker" ref={colourPicker} style={{ display: "none" }} onChange={() => setPickerVal()} />