Files
color/xterm.go
T

172 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}
// 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
}