Skip to content

Instantly share code, notes, and snippets.

@partybusiness
Last active May 21, 2025 00:27
Show Gist options
  • Save partybusiness/c59460fc2d475672c8a09399b4853b1d to your computer and use it in GitHub Desktop.
Save partybusiness/c59460fc2d475672c8a09399b4853b1d to your computer and use it in GitHub Desktop.
Trying out different ways of blending colours in different colour spaces.
shader_type canvas_item;
uniform sampler2D textureA:source_color;
uniform sampler2D textureB:source_color;
uniform float blend:hint_range(0.0, 1.0, 0.01) = 0.5;
// lowers saturation for colours that pass through the centre of hue space
uniform float pow_comp:hint_range(0.0, 10.0, 0.01) = 1.0;
// https://github.com/scanberg/viamd/blob/da84469033c799e0a0035fbf96919f1eb84fa808/src/color_utils.h#L67
vec3 hcl_to_rgb(vec3 HCL) {
if (HCL.r < 0.0)
HCL.r += 1.0;
const float HCLgamma = 3.0;
const float HCLy0 = 100.0;
const float HCLmaxL = 0.530454533953517f; // == exp(HCLgamma / HCLy0) - 0.5
vec3 RGB = vec3(0.0);
if (HCL.z != 0.0) {
float H = HCL.x;
float C = HCL.y;
float L = HCL.z * HCLmaxL;
float Q = exp((1.0 - C / (2.0 * L)) * (HCLgamma / HCLy0));
float U = (2.0 * L - C) / (2.0 * Q - 1.0);
float V = C / Q;
float T = tan((H + min(fract(2.0 * H) / 4.0, fract(-2.0 * H) / 8.0)) * PI * 2.0);
H *= 6.0;
if (H <= 1.0) {
RGB.r = 1.0;
RGB.g = T / (1.0 + T);
} else if (H <= 2.0) {
RGB.r = (1.0 + T) / T;
RGB.g = 1.0;
} else if (H <= 3.0) {
RGB.g = 1.0;
RGB.b = 1.0 + T;
} else if (H <= 4.0) {
RGB.g = 1.0 / (1.0 + T);
RGB.b = 1.0;
} else if (H <= 5.0) {
RGB.r = -1.0 / T;
RGB.b = 1.0;
} else {
RGB.r = 1.0;
RGB.b = -T;
}
RGB = RGB * V + U;
}
return RGB;
}
vec3 rgb_to_hcl(vec3 rgb) {
const float HCLgamma = 3.0;
const float HCLy0 = 100.0;
const float HCLmaxL = 0.530454533953517f; // == exp(HCLgamma / HCLy0) - 0.5
vec3 HCL;
float H = 0.0;
float U = min(rgb.x, min(rgb.g, rgb.b));
float V = max(rgb.x, max(rgb.g, rgb.b));
float Q = HCLgamma / HCLy0;
HCL.y = V - U;
if (HCL.y != 0.0) {
H = atan(rgb.g - rgb.b, rgb.x - rgb.g) / 3.1515926535f;
Q *= U / V;
}
Q = exp(Q);
HCL.x = fract(H / 2.0 - min(fract(H), fract(-H)) / 6.0);
HCL.y *= Q;
HCL.z = mix(-U, V, Q) / (HCLmaxL * 2.0);
return HCL;
}
vec2 hue_to_pos(float hue) {
return (vec2(cos(hue * TAU), sin(hue * TAU)));
}
float pos_to_hue(vec2 pos) {
return atan(pos.y, pos.x) / TAU;
}
void fragment() {
vec3 sampleA = rgb_to_hcl(texture(textureA, UV).rgb);
vec3 sampleB = rgb_to_hcl(texture(textureB, UV).rgb);
vec3 blended;
blended.rgb = mix(sampleA.rgb, sampleB.rgb, blend);
// handle hue blending as special
vec2 mix_pos = mix(hue_to_pos(sampleA.r), hue_to_pos(sampleB.r), blend);
blended.r = pos_to_hue(mix_pos);
//blended.rgb = sampleA.rgb;
// desaturate when hue is traveling far
blended.g = blended.g * pow(length(mix_pos), pow_comp);//blended.g * pow(length(mix_pos), 2.0);
//blended.r = sampleA.r;
COLOR.rgb = hcl_to_rgb(blended);
}
shader_type canvas_item;
uniform sampler2D textureA:source_color;
uniform sampler2D textureB:source_color;
uniform float blend:hint_range(0.0, 1.0, 0.01);
// lowers saturation for colours that pass through the centre of hue space
uniform float pow_comp:hint_range(0.0, 50.0, 0.01) = 1.0;
float hue_to_rgb(float p, float q, float t){
if(t < 0.0) t += 1.0;
if(t > 1.0) t -= 1.0;
if(t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if(t < 1.0/2.0) return q;
if(t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl_to_rgb(vec3 hsl) {
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
vec3 result;
if (s == 0.0) {
result.rgb = vec3(l);
} else {
float q, p;
if (l < 0.5) {
q = l * (1.0 + s);
} else {
q = l + s - l * s;
}
p = 2.0 * l - q;
result.r = hue_to_rgb(p, q, h + 1.0 / 3.0);
result.g = hue_to_rgb(p, q, h);
result.b = hue_to_rgb(p, q, h - 1.0 / 3.0);
}
return result;
}
vec3 rgb_to_hsl(vec3 col){
float c_max = max(col.r, max(col.g, col.b));
float c_min = min(col.r, min(col.g, col.b));
float h, s, l = (c_max + c_min) / 2.0;
if(c_max == c_min) {
h = s = 0.0; // achromatic
} else {
float d = c_max - c_min;
s = l > 0.5 ? d / (2.0 - c_max - c_min) : d / (c_max + c_min);
if (c_max == col.r) {
h = (col.g - col.b) / d + (col.g < col.b ? 6.0 : 0.0);
} else if (c_max == col.g) {
h = (col.b - col.r) / d + 2.0;
} else {
h = (col.r - col.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
vec2 hue_to_pos(float hue) {
return (vec2(cos(hue * TAU), sin(hue * TAU)));
}
float pos_to_hue(vec2 pos) {
return atan(pos.y, pos.x) / TAU;
}
void fragment() {
vec3 sampleA = rgb_to_hsl(texture(textureA, UV).rgb);
vec3 sampleB = rgb_to_hsl(texture(textureB, UV).rgb);
vec3 blended;
blended.gb = mix(sampleA.gb, sampleB.gb, blend);
// handle hue blending as special
vec2 mix_pos = mix(hue_to_pos(sampleA.r), hue_to_pos(sampleB.r), blend);
blended.r = pos_to_hue(mix_pos);
// desaturate when hue is traveling far
blended.g = blended.g * pow(length(mix_pos), pow_comp);//blended.g * pow(length(mix_pos), 2.0);
//blended.r = sampleA.r;
COLOR.rgb = hsl_to_rgb(blended);
}
shader_type canvas_item;
uniform sampler2D textureA:source_color;
uniform sampler2D textureB:source_color;
uniform float blend:hint_range(0.0, 1.0, 0.01) = 0.5;
// lowers saturation for colours that pass through the centre of hue space
uniform float pow_comp:hint_range(0.0, 10.0, 0.01) = 1.0;
float hue_to_rgb(float p, float q, float t){
if(t < 0.0) t += 1.0;
if(t > 1.0) t -= 1.0;
if(t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if(t < 1.0/2.0) return q;
if(t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsv_to_rgb(vec3 hsl) {
float h = hsl.x;
if (h < 0.0)
h += 1.0;
if (h > 1.0)
h -= 1.0;
float s = hsl.y;
float v = hsl.z;
vec3 result;
float i = floor(h * 6.0);
float f = h * 6.0 - i;
float p = v * (1.0 - s);
float q = v * (1.0 - f * s);
float t = v * (1.0 - (1.0 - f) * s);
if (abs(i) < 0.1) result.rgb = vec3(v,t,p);
else if (abs(i - 1.0) < 0.1) result.rgb = vec3(q, v, p);
else if (abs(i - 2.0) < 0.1) result.rgb = vec3(p, v, t);
else if (abs(i - 3.0) < 0.1) result.rgb = vec3(p, q, v);
else if (abs(i - 4.0) < 0.1) result.rgb = vec3(t, p, v);
else result.rgb = vec3(v, p, q);
// result.rgb = vec3(1.0, 1.0, 1.00);
return result;
}
vec3 rgb_to_hsv(vec3 col){
float c_max = max(col.r, max(col.g, col.b));
float c_min = min(col.r, min(col.g, col.b));
float h, s, v = c_max;
float d = c_max - c_min;
s = c_max == 0.0 ? 0.0 : d / c_max;
if(c_max == c_min) {
h = 0.0; // achromatic
} else {
if (c_max == col.r) {
h = (col.g - col.b) / d + (col.g < col.b ? 6.0 : 0.0);
} else if (c_max == col.g) {
h = (col.b - col.r) / d + 2.0;
} else {
h = (col.r - col.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, v);
}
vec2 hue_to_pos(float hue) {
return (vec2(cos(hue * TAU), sin(hue * TAU)));
}
float pos_to_hue(vec2 pos) {
return atan(pos.y, pos.x) / TAU;
}
void fragment() {
vec3 sampleA = rgb_to_hsv(texture(textureA, UV).rgb);
vec3 sampleB = rgb_to_hsv(texture(textureB, UV).rgb);
vec3 blended;
blended.gb = mix(sampleA.gb, sampleB.gb, blend);
// handle hue blending as special
vec2 mix_pos = mix(hue_to_pos(sampleA.r), hue_to_pos(sampleB.r), blend);
blended.r = pos_to_hue(mix_pos);
//blended.rgb = sampleA.rgb;
// desaturate when hue is traveling far
blended.g = blended.g * pow(length(mix_pos), pow_comp);//blended.g * pow(length(mix_pos), 2.0);
//blended.r = sampleA.r;
COLOR.rgb = hsv_to_rgb(blended);
}
shader_type canvas_item;
uniform sampler2D textureA:source_color;
uniform sampler2D textureB:source_color;
uniform float blend:hint_range(0.0, 1.0, 0.01);
void fragment() {
vec3 sampleA = texture(textureA, UV).rgb;
vec3 sampleB = texture(textureB, UV).rgb;
COLOR.rgb = mix(sampleA, sampleB, blend);
}
shader_type canvas_item;
uniform sampler2D textureA:source_color;
uniform sampler2D textureB:source_color;
uniform float blend:hint_range(0.0, 1.0, 0.01) = 0.5;
// lowers saturation for colours that pass through the centre of hue space
uniform float pow_comp:hint_range(0.0, 10.0, 0.01) = 1.0;
//https://github.com/joeedh/pigment-painter/blob/c358be306967e53d5a8582e990611d88b3a54d50/wasm/color.h#L62
vec3 rgb_to_xyz(vec3 rgb) {
float var_R = rgb.r;
float var_G = rgb.g;
float var_B = rgb.b;
// ignor gamma for now
/*if (!noGamma) {
if (var_R > 0.04045)
var_R = powf((var_R + 0.055) / 1.055, 2.4);
else
var_R = var_R / 12.92;
if (var_G > 0.04045)
var_G = powf((var_G + 0.055) / 1.055, 2.4);
else
var_G = var_G / 12.92;
if (var_B > 0.04045)
var_B = powf((var_B + 0.055) / 1.055, 2.4);
else
var_B = var_B / 12.92;
}*/
/*
on factor;
off period;
f1 := var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805 - X;
f2 := var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722 - Y;
f3 := var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505 - Z;
f := solve({f1, f2, f3}, {var_r, var_g, var_b});
*/
// Observer. = 2��, Illuminant = D65
float X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805;
float Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722;
float Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505;
return vec3(X, Y, Z);
}
vec3 xyz_to_rgb(vec3 xyz) {
float var_X = xyz.x; // X from 0 to 95.047 (Observer = 2��, Illuminant = D65)
float var_Y = xyz.y; // Y from 0 to 100.000
float var_Z = xyz.z; // Z from 0 to 108.883
float var_R = var_X * 3.240625 + var_Y * -1.53720797 + var_Z * -0.498628;
float var_G = var_X * -0.9689307 + var_Y * 1.87575606 + var_Z * 0.04151752;
float var_B = var_X * 0.0557101 + var_Y * -0.204021 + var_Z * 1.05699;
/* if (!noGamma) {
if (var_R > 0.003130807)
var_R = 1.055 * (powf(var_R, 1.0 / 2.4)) - 0.055;
else
var_R = 12.92 * var_R;
if (var_G > 0.003130807)
var_G = 1.055 * (powf(var_G, 1.0 / 2.4)) - 0.055;
else
var_G = 12.92 * var_G;
if (var_B > 0.003130807)
var_B = 1.055 * (powf(var_B, 1.0 / 2.4)) - 0.055;
else
var_B = 12.92 * var_B;
}*/
return vec3(var_R, var_G, var_B);
}
void fragment() {
vec3 sampleA = rgb_to_xyz(texture(textureA, UV).rgb);
vec3 sampleB = rgb_to_xyz(texture(textureB, UV).rgb);
vec3 blended;
blended.rgb = mix(sampleA.rgb, sampleB.rgb, blend);
// handle hue blending as special
//blended.r = sampleA.r;
COLOR.rgb = xyz_to_rgb(blended);
}
shader_type canvas_item;
uniform sampler2D textureA:source_color;
uniform sampler2D textureB:source_color;
uniform float blend:hint_range(0.0, 1.0, 0.01);
//based on https://www.shadertoy.com/view/3lycWz
vec3 rgb_to_yuv(vec3 rgb) {
float y = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
return vec3(y, 0.493 * (rgb.b - y), 0.877 * (rgb.r - y));
}
vec3 yuv_to_rgb(vec3 yuv) {
float y = yuv.x;
float u = yuv.y;
float v = yuv.z;
vec3 rgb = vec3(
y + 1.0 / 0.877 * v,
y - 0.39393 * u - 0.58081 * v,
y + 1.0 / 0.493 * u
);
return rgb;
}
void fragment() {
vec3 sampleA = rgb_to_yuv(texture(textureA, UV).rgb);
vec3 sampleB = rgb_to_yuv(texture(textureB, UV).rgb);
vec3 blended = mix(sampleA, sampleB, blend);
COLOR.rgb = yuv_to_rgb(blended);
}
@partybusiness
Copy link
Author

I found that the main difference is between colour spaces that use a hue and ones that don't.

There isn't much point in blending in YUV or XYZ over RGB unless you're doing something else with it.

The hsl, hcl, hsv all rely on hue as one property, where I would convert to a 2D vector and then convert back. So, rather than evenly spaced movement from one hue to the other, it spends more time on either end. And also allowed me to try desaturating during the transition based on the magnitude of the vector. There might be some effects where it might looks good to play with hue in these ways, but for most cases of blending between two images, it ends up feeling oversaturated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment