Skip to content

Instantly share code, notes, and snippets.

@MarkJeronimus
Last active April 6, 2025 10:14
Show Gist options
  • Select an option

  • Save MarkJeronimus/b0b3eb1f59c0f0efd1525f439c01b2e9 to your computer and use it in GitHub Desktop.

Select an option

Save MarkJeronimus/b0b3eb1f59c0f0efd1525f439c01b2e9 to your computer and use it in GitHub Desktop.
Converting sRGB to HCI and back
Notes on this Java snippet:
- I left out 'Math.' because it's boilerplate/noise
- Color3f is a simple read/write container where the components are float instead of int
- Implementation of 'fromSRGB' and 'toSRGB' should be straightforward
- Uses tab characters for initial indentation, continues with space characters for vertical alignment purposes
// HCI /////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* This color space is a hexagonal bipyramid with the grays at the central axis
* and the primary and secondary colors on a hexagonal plane at the center.
* <p>
* Compared to HSL, Saturation is replaced with Chroma my multiplying by Lightness,
* and Lightness is replaced by perceptual Intensity.
* <p>
* The input is assumed to be sRGB, and the output Intensity is linear.
*/
public static Color3f rgb2hci(Color3f color) {
float min = min(min(color.r, color.g), color.b);
float max = max(max(color.r, color.g), color.b);
float chroma = max - min;
if (chroma < 1.0e-7f) {
float intensity = fromSRGB(max);
return new Color3f(0.0f, 0.0f, intensity);
}
float hue = rgb2hueInternal(color.r, color.g, color.b, min, max, chroma);
float intensity = getPerceptualLuminosity(color.r, color.g, color.b);
return new Color3f(hue, chroma, intensity);
}
public static Color3f hci2rgb(Color3f color) {
float hue6 = (color.r - (float)floor(color.r)) * 6;
// Hue & sat coding
float r = max(0.0f, min(1.0f, abs(hue6 - 3.0f) - 1.0f)) * color.g;
float g = max(0.0f, min(1.0f, 2.0f - abs(hue6 - 2.0f))) * color.g;
float b = max(0.0f, min(1.0f, 2.0f - abs(hue6 - 4.0f))) * color.g;
// Luma searching
if (color.b < getPerceptualLuminosity(r, g, b)) {
return findDarkerColor(r, g, b, color.b);
} else {
return findBrighterColor(r, g, b, color.b);
}
}
private static float rgb2hueInternal(float r, float g, float b, float min, float max, float chroma) {
assert chroma != 0.0f;
if (max == r) {
if (min == b) {
return ((g - b) / chroma) / 6.0f;
} else {
return ((g - b) / chroma + 6.0f) / 6.0f;
}
} else {
if (max == g) {
return ((b - r) / chroma + 2.0f) / 6.0f;
} else {
return ((r - g) / chroma + 4.0f) / 6.0f;
}
}
}
public static float getPerceptualLuminosity(float r, float g, float b) {
return fromSRGB(r) * 0.2126f + fromSRGB(g) * 0.7152f + fromSRGB(b) * 0.0722f;
}
private static Color3f findDarkerColor(float r, float g, float b, float targetIntensity) {
float minMul = 0.0f;
float maxMul = 1.0f;
float mul = 0.0f;
float intensity;
while (maxMul - minMul > 1.0e-7f) {
mul = (maxMul + minMul) / 2;
intensity = getPerceptualLuminosity(r * mul, g * mul, b * mul);
if (intensity < targetIntensity) {
minMul = mul;
} else {
maxMul = mul;
}
}
return new Color3f(r * mul, g * mul, b * mul);
}
private static Color3f findBrighterColor(float r, float g, float b, float targetIntensity) {
float minAdd = 0.0f;
float maxAdd = 1.0f;
float add = 0.0f;
float intensity;
while (maxAdd - minAdd > 1.0e-7f) {
add = (maxAdd + minAdd) / 2;
intensity = getPerceptualLuminosity(min(r + add, 1.0f),
min(g + add, 1.0f),
min(b + add, 1.0f));
if (intensity < targetIntensity) {
minAdd = add;
} else {
maxAdd = add;
}
}
return new Color3f(min(r + add, 1.0f),
min(g + add, 1.0f),
min(b + add, 1.0f));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment