The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#define Uses_TColorAttr
#include <tvision/tv.h>
#include <internal/constarr.h>
namespace tvision
{
//// RGB to XTerm16 conversion algorithm
//
// XTerm16 is actually what's known as a 4-bit RGBI color palette, which
// is not regular. Many solutions out there overlook this fact. Others rely
// on the so-called CIEDE2000 formula, which doesn't convince me either.
//
// The algorithm here consists in converting RGB to HSL as an intermediate
// step. Then we do the following:
//
// * Decide whether the color should be approximated to grayscale or not.
// * If it is grayscale, pick between the 4 levels of gray. Otherwise, pick
// between the dark and bright color variants. The L component is used for this.
// * If it is color, choose the final color based on the H component.
//
// The result is perceptually closer to the original than the other solutions
// I have seen around. Additionally, this algorithm can be computed in real-time.
//
// This implementation uses integer arithmetic and performs at most one integer
// division.
struct HCL
{
uint8_t h; // [0..HUE_MAX)
uint8_t c; // [0..255]
uint8_t l; // [0..255]
};
constexpr uint8_t HUE_PRECISION = 32;
constexpr uint8_t HUE_MAX = 6*HUE_PRECISION;
static constexpr
HCL RGBtoHCL(uint8_t R, uint8_t G, uint8_t B) noexcept
{
uint8_t Xmin = min(min(R, G), B);
uint8_t Xmax = max(max(R, G), B);
uint8_t V = Xmax;
uint8_t L = uint16_t(Xmax + Xmin)/2;
uint8_t C = Xmax - Xmin;
int16_t H = 0;
if (C)
{
if (V == R)
H = int16_t(HUE_PRECISION*(G - B))/C;
else if (V == G)
H = int16_t(HUE_PRECISION*(B - R))/C + 2*HUE_PRECISION;
else if (V == B)
H = int16_t(HUE_PRECISION*(R - G))/C + 4*HUE_PRECISION;
if (H < 0)
H += HUE_MAX;
else if (H >= HUE_MAX)
H -= HUE_MAX;
}
return {(uint8_t) H, C, L};
}
static constexpr uint8_t u8(double d) noexcept
{
return uint8_t(d*255);
}
static constexpr
uint8_t RGBtoXTerm16(uint8_t r, uint8_t g, uint8_t b) noexcept
{
HCL c = RGBtoHCL(r, g, b);
if (c.c >= 12) // Color if Chroma >= 12.
{
constexpr uint8_t normal[6] = {0x1, 0x3, 0x2, 0x6, 0x4, 0x5};
constexpr uint8_t bright[6] = {0x9, 0xB, 0xA, 0xE, 0xC, 0xD};
uint8_t index = uint8_t(c.h < HUE_MAX - HUE_PRECISION/2 ?
c.h + HUE_PRECISION/2
: c.h - (HUE_MAX - HUE_PRECISION/2)
)/HUE_PRECISION;
if (c.l < u8(0.5))
return normal[index];
if (c.l < u8(0.925))
return bright[index];
return 15;
}
else
{
if (c.l < u8(0.25))
return 0;
if (c.l < u8(0.625))
return 8;
if (c.l < u8(0.875))
return 7;
return 15;
}
}
static constexpr
constarray<uint8_t, 256> initXTerm256toXTerm16LUT() noexcept
{
constarray<uint8_t, 256> res {};
for (uint8_t i = 0; i < 16; ++i)
res[i] = i;
for (uint8_t i = 0; i < 6; ++i)
{
uint8_t R = i ? 55 + i*40 : 0;
for (uint8_t j = 0; j < 6; ++j)
{
uint8_t G = j ? 55 + j*40 : 0;
for (uint8_t k = 0; k < 6; ++k)
{
uint8_t B = k ? 55 + k*40 : 0;
uint8_t idx16 = RGBtoXTerm16(R, G, B);
res[16 + (i*6 + j)*6 + k] = idx16;
}
}
}
for (uint8_t i = 0; i < 24; ++i)
{
uint8_t L = i * 10 + 8;
uint8_t idx16 = RGBtoXTerm16(L, L, L);
res[232 + i] = idx16;
}
return res;
}
static constexpr
uint32_t pack(uint8_t R, uint8_t G, uint8_t B) noexcept
{
return (((R << 8) | G) << 8) | B;
};
static constexpr
constarray<uint32_t, 256> initXTerm256toRGBLUT() noexcept
{
// Indices 16..255 only.
constarray<uint32_t, 256> res {};
for (uint8_t i = 0; i < 6; ++i)
{
uint8_t R = i ? 55 + i*40 : 0;
for (uint8_t j = 0; j < 6; ++j)
{
uint8_t G = j ? 55 + j*40 : 0;
for (uint8_t k = 0; k < 6; ++k)
{
uint8_t B = k ? 55 + k*40 : 0;
res[16 + (i*6 + j)*6 + k] = pack(R, G, B);
}
}
}
for (uint8_t i = 0; i < 24; ++i)
{
uint8_t L = i * 10 + 8;
res[232 + i] = pack(L, L, L);
}
return res;
}
extern constexpr
constarray<uint8_t, 256> XTerm256toXTerm16LUT = initXTerm256toXTerm16LUT();
extern constexpr
constarray<uint32_t, 256> XTerm256toRGBLUT = initXTerm256toRGBLUT();
uint8_t RGBtoXTerm16Impl(TColorRGB c) noexcept
{
return RGBtoXTerm16(c.r, c.g, c.b);
}
} // namespace tvision