213 lines
4.9 KiB
Go
213 lines
4.9 KiB
Go
package color
|
||
|
||
import (
|
||
"sync"
|
||
"sync/atomic"
|
||
)
|
||
|
||
// Generic xterm 256-color palette indices
|
||
// Color cube: index = 16 + 36*r + 6*g + b where r,g,b ∈ [0,5]
|
||
// Grayscale ramp: indices 232-255, level = 8 + 10*(index-232)
|
||
// Ordered dark-to-light within each hue group
|
||
|
||
const (
|
||
P256DeepNavy uint8 = 17 // (0,0,1)
|
||
P256DarkBlue uint8 = 18 // (0,0,2)
|
||
P256SteelBlue uint8 = 75 // (1,3,5)
|
||
P256LightBlue uint8 = 81 // (1,4,5)
|
||
|
||
P256DeepTeal uint8 = 23 // (0,1,1)
|
||
P256Teal uint8 = 44 // (0,4,4)
|
||
P256Green uint8 = 46 // (0,5,0)
|
||
P256Cyan uint8 = 51 // (0,5,5)
|
||
P256LightCyan uint8 = 87 // (1,5,5)
|
||
|
||
P256CobaltBlue uint8 = 33 // (0,2,5)
|
||
P256DarkPurpleBlue uint8 = 54 // (1,0,2)
|
||
P256Indigo uint8 = 63 // (1,1,5)
|
||
P256Purple uint8 = 129 // (3,0,5)
|
||
P256Violet uint8 = 134 // (3,1,4)
|
||
P256MediumPurple uint8 = 135 // (3,1,5)
|
||
P256Orchid uint8 = 176 // (4,2,4)
|
||
|
||
P256YellowGreen uint8 = 154 // (3,5,0)
|
||
|
||
P256Maroon uint8 = 52 // (1,0,0)
|
||
P256DarkCrimson uint8 = 88 // (2,0,0)
|
||
P256Crimson uint8 = 160 // (4,0,0)
|
||
|
||
P256Red uint8 = 196 // (5,0,0)
|
||
P256Rose uint8 = 198 // (5,0,2)
|
||
P256RedOrange uint8 = 202 // (5,1,0)
|
||
P256Orange uint8 = 208 // (5,2,0)
|
||
P256Amber uint8 = 214 // (5,3,0)
|
||
P256Gold uint8 = 220 // (5,4,0)
|
||
P256Yellow uint8 = 226 // (5,5,0)
|
||
|
||
P256DarkAmber uint8 = 94 // (2,1,0)
|
||
|
||
P256Gray uint8 = 240 // Grayscale step 8, level ~88
|
||
)
|
||
|
||
// 6-bit quantized LUT for Redmean-based 256-color mapping
|
||
// 64×64×64 = 262,144 bytes, fits in L2 cache
|
||
const lut256Size = 64 * 64 * 64
|
||
|
||
var (
|
||
lut256Ptr atomic.Pointer[[lut256Size]uint8]
|
||
lut256Once sync.Once
|
||
)
|
||
|
||
// lut256 returns the palette LUT, building it on first use.
|
||
func lut256() *[lut256Size]uint8 {
|
||
if p := lut256Ptr.Load(); p != nil {
|
||
return p
|
||
}
|
||
return lut256Build()
|
||
}
|
||
|
||
//go:noinline
|
||
func lut256Build() *[lut256Size]uint8 {
|
||
lut256Once.Do(func() {
|
||
t := new([lut256Size]uint8)
|
||
for r := range 64 {
|
||
for g := range 64 {
|
||
for b := range 64 {
|
||
c := RGB{
|
||
R: uint8(r<<2 | 2),
|
||
G: uint8(g<<2 | 2),
|
||
B: uint8(b<<2 | 2),
|
||
}
|
||
t[r<<12|g<<6|b] = computeRedmean256(c)
|
||
}
|
||
}
|
||
}
|
||
lut256Ptr.Store(t)
|
||
})
|
||
return lut256Ptr.Load()
|
||
}
|
||
|
||
// WarmXterm256 forces LUT construction. Idempotent, safe for concurrent use.
|
||
// Optional, prevents latency spike on first RGBTo256 render in the terminal loop.
|
||
func WarmXterm256() { _ = lut256() }
|
||
|
||
func computeRedmean256(c RGB) uint8 {
|
||
if c.R == c.G && c.G == c.B {
|
||
if c.R < 8 {
|
||
return 16
|
||
}
|
||
if c.R > 238 {
|
||
return 231
|
||
}
|
||
return uint8(232 + (int(c.R)-8)/10)
|
||
}
|
||
|
||
bestIdx := uint8(16)
|
||
minDist := 1 << 30
|
||
|
||
for i := range 216 {
|
||
cand := RGB{
|
||
R: cubeValues[i/36],
|
||
G: cubeValues[(i/6)%6],
|
||
B: cubeValues[i%6],
|
||
}
|
||
if d := RedmeanDistance(c, cand); d < minDist {
|
||
minDist = d
|
||
bestIdx = uint8(16 + i)
|
||
}
|
||
}
|
||
|
||
for i := range 24 {
|
||
g := uint8(8 + i*10)
|
||
if d := RedmeanDistance(c, RGB{R: g, G: g, B: g}); d < minDist {
|
||
minDist = d
|
||
bestIdx = uint8(232 + i)
|
||
}
|
||
}
|
||
|
||
return bestIdx
|
||
}
|
||
|
||
var cubeValues = [6]uint8{0, 95, 135, 175, 215, 255}
|
||
|
||
// ANSI 16 color indices, sent as SGR 30-37/90-97 (40-47/100-107 background).
|
||
// The terminal's theme picks the shade; a text console renders them too.
|
||
const (
|
||
ANSIBlack uint8 = iota
|
||
ANSIRed
|
||
ANSIGreen
|
||
ANSIYellow
|
||
ANSIBlue
|
||
ANSIMagenta
|
||
ANSICyan
|
||
ANSIWhite
|
||
ANSIBrightBlack
|
||
ANSIBrightRed
|
||
ANSIBrightGreen
|
||
ANSIBrightYellow
|
||
ANSIBrightBlue
|
||
ANSIBrightMagenta
|
||
ANSIBrightCyan
|
||
ANSIBrightWhite
|
||
)
|
||
|
||
// xterm's default shades for the ANSI 16 indices, the reference for RGBTo16
|
||
var ansi16 = [16]RGB{
|
||
{0, 0, 0}, {205, 0, 0}, {0, 205, 0}, {205, 205, 0},
|
||
{0, 0, 238}, {205, 0, 205}, {0, 205, 205}, {229, 229, 229},
|
||
{127, 127, 127}, {255, 0, 0}, {0, 255, 0}, {255, 255, 0},
|
||
{92, 92, 255}, {255, 0, 255}, {0, 255, 255}, {255, 255, 255},
|
||
}
|
||
|
||
// RGBTo16 converts 24-bit RGB to the nearest ANSI 16 index (Redmean distance
|
||
// to xterm's default shades), for terminals limited to 16 colors.
|
||
func RGBTo16(c RGB) uint8 {
|
||
best, minDist := ANSIBlack, 1<<30
|
||
for i, cand := range ansi16 {
|
||
if d := RedmeanDistance(c, cand); d < minDist {
|
||
best, minDist = uint8(i), d
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
// RGBTo256 converts 24-bit RGB to the nearest xterm-256 palette index.
|
||
// O(1) via the Redmean LUT; the first call builds it.
|
||
func RGBTo256(c RGB) uint8 {
|
||
return lut256()[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)]
|
||
}
|
||
|
||
// Cube256 returns the xterm 256-palette index for an RGB cube coordinate.
|
||
func Cube256(r, g, b uint8) uint8 {
|
||
if r > 5 {
|
||
r = 5
|
||
}
|
||
if g > 5 {
|
||
g = 5
|
||
}
|
||
if b > 5 {
|
||
b = 5
|
||
}
|
||
return 16 + 36*r + 6*g + b
|
||
}
|
||
|
||
// CubeRGB256 returns the (r, g, b) cube coordinates for a 256-palette color cube index.
|
||
func CubeRGB256(index uint8) (r, g, b uint8) {
|
||
if index < 16 || index > 231 {
|
||
return 0, 0, 0
|
||
}
|
||
n := index - 16
|
||
r = n / 36
|
||
g = (n % 36) / 6
|
||
b = n % 6
|
||
return r, g, b
|
||
}
|
||
|
||
// Gray256 returns the xterm 256-palette index for a grayscale step.
|
||
func Gray256(step uint8) uint8 {
|
||
if step > 23 {
|
||
step = 23
|
||
}
|
||
return 232 + step
|
||
}
|