Last active
May 3, 2026 07:38
-
-
Save greggman/0924097afdde605db6cc551dd664dd8e to your computer and use it in GitHub Desktop.
WebGL: lerp rgb colors in hsl space.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| html, body { | |
| margin: 0; | |
| height: 100%; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <canvas></canvas> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as twgl from 'https://twgljs.org/dist/5.x/twgl-full.module.js'; | |
| const { m4 } = twgl; | |
| const vs = `#version 300 es | |
| layout(location = 0) in vec4 position; | |
| layout(location = 1) in float l; | |
| out float v_l; | |
| uniform mat4 matrix; | |
| void main() { | |
| gl_Position = matrix * vec4(position); | |
| v_l = l; | |
| } | |
| ` | |
| const fs1 = `#version 300 es | |
| precision highp float; | |
| in float v_l; | |
| out vec4 fragColor; | |
| void main() { | |
| fragColor = vec4(v_l, v_l, v_l, 1); | |
| } | |
| ` | |
| const fs2 = `#version 300 es | |
| precision highp float; | |
| in float v_l; | |
| out vec4 fragColor; | |
| uniform vec4 color1; | |
| uniform vec4 color2; | |
| void main() { | |
| fragColor = mix(color1, color2, v_l); | |
| } | |
| `; | |
| const fs3 = `#version 300 es | |
| precision highp float; | |
| in float v_l; | |
| out vec4 fragColor; | |
| uniform vec4 color1; | |
| uniform vec4 color2; | |
| float Epsilon = 1e-10; | |
| vec3 rgbToHcv(vec3 rgb) { | |
| // Based on work by Sam Hocevar and Emil Persson | |
| vec4 P = mix(vec4(rgb.bg, -1.0, 2.0/3.0), vec4(rgb.gb, 0.0, -1.0/3.0), step(rgb.b, rgb.g)); | |
| vec4 Q = mix(vec4(P.xyw, rgb.r), vec4(rgb.r, P.yzx), step(P.x, rgb.r)); | |
| float C = Q.x - min(Q.w, Q.y); | |
| float H = abs((Q.w - Q.y) / (6.0 * C + Epsilon) + Q.z); | |
| return vec3(H, C, Q.x); | |
| } | |
| vec3 rgbToHsl(vec3 rgb) { | |
| vec3 hcv = rgbToHcv(rgb); | |
| float L = hcv.z - hcv.y * 0.5; | |
| float S = hcv.y / (1.0 - abs(L * 2.0 - 1.0) + Epsilon); | |
| return vec3(hcv.x, S, L); | |
| } | |
| vec3 hslToRgb(vec3 hsl) { | |
| vec3 c = vec3(fract(hsl.x), clamp(vec2(hsl.y, hsl.z), vec2(0), vec2(1))); | |
| vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, vec3(0), vec3(1)); | |
| return c.z + c.y * (rgb - 0.5) * (1.0 - abs(2.0 * c.z - 1.0)); | |
| } | |
| float mixCircular(float a, float b, float t) { | |
| float diff = fract(b - a + 0.5) - 0.5; | |
| return fract(a + diff * t); | |
| } | |
| void main() { | |
| vec3 hsl1 = rgbToHsl(color1.rgb); | |
| vec3 hsl2 = rgbToHsl(color2.rgb); | |
| vec3 hsl = vec3( | |
| mixCircular(hsl1.x, hsl2.x, v_l), // lerp the hue around a circle | |
| mix(hsl1.yz, hsl2.yz, v_l)); // lerp the sat and lightness as normal | |
| vec3 rgb = hslToRgb(hsl); | |
| fragColor = vec4(rgb, 1); | |
| } | |
| ` | |
| const canvas = document.querySelector("canvas"); | |
| twgl.resizeCanvasToDisplaySize(canvas); | |
| const gl = canvas.getContext('webgl2', {alpha: false}); | |
| const posLoc = 0; // set in shader | |
| const lLoc = 1; // set in shader | |
| const position = new Float32Array([ | |
| -1, -1, 1, -1, -1, 1, | |
| -1, 1, 1, -1, 1, 1, | |
| ]); | |
| const l = new Float32Array([ | |
| 0, 1, 0, | |
| 0, 1, 1, | |
| ]); | |
| const positionBuffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW); | |
| gl.enableVertexAttribArray(posLoc); | |
| gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0); | |
| const lBuffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, lBuffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, l, gl.STATIC_DRAW); | |
| gl.enableVertexAttribArray(lLoc); | |
| gl.vertexAttribPointer(lLoc, 1, gl.FLOAT, false, 0, 0); | |
| const programInfo1 = twgl.createProgramInfo(gl, [vs, fs1]); | |
| const programInfo2 = twgl.createProgramInfo(gl, [vs, fs2]); | |
| const programInfo3 = twgl.createProgramInfo(gl, [vs, fs3]); | |
| gl.useProgram(programInfo1.program); | |
| twgl.setUniforms(programInfo1, { | |
| matrix: m4.scale(m4.translation([0, 0.5, 0]), [1, 0.2, 1]), | |
| }); | |
| gl.drawArrays(gl.TRIANGLES, 0, 6); | |
| gl.useProgram(programInfo2.program); | |
| twgl.setUniforms(programInfo2, { | |
| matrix: m4.scale(m4.translation([0, 0, 0]), [1, 0.2, 1]), | |
| color1: [1, 0, 0, 1], | |
| color2: [0, 0, 1, 1], | |
| }); | |
| gl.drawArrays(gl.TRIANGLES, 0, 6); | |
| gl.useProgram(programInfo3.program); | |
| twgl.setUniforms(programInfo3, { | |
| matrix: m4.scale(m4.translation([0, -0.5, 0]), [1, 0.2, 1]), | |
| color1: [1, 0, 0, 1], | |
| color2: [0, 0, 1, 1], | |
| }); | |
| gl.drawArrays(gl.TRIANGLES, 0, 6); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {"name":"WebGL: lerp rgb colors in hsl space.","settings":{},"filenames":["index.html","index.css","index.js"]} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment