v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package terminal
|
||||
|
||||
// ColorMode indicates terminal color capability
|
||||
type ColorMode uint8
|
||||
|
||||
const (
|
||||
ColorMode256 ColorMode = iota // xterm-256 palette
|
||||
ColorModeTrueColor // 24-bit RGB
|
||||
)
|
||||
|
||||
// RGB represents a 24-bit color
|
||||
type RGB struct {
|
||||
R uint8 `toml:"r"`
|
||||
G uint8 `toml:"g"`
|
||||
B uint8 `toml:"b"`
|
||||
}
|
||||
|
||||
// Lerp linearly interpolates from c to other by t, clamped to [0,1]
|
||||
func (c RGB) Lerp(other RGB, t float64) RGB {
|
||||
if t <= 0 {
|
||||
return c
|
||||
}
|
||||
if t >= 1 {
|
||||
return other
|
||||
}
|
||||
return RGB{
|
||||
R: uint8(float64(c.R) + (float64(other.R)-float64(c.R))*t),
|
||||
G: uint8(float64(c.G) + (float64(other.G)-float64(c.G))*t),
|
||||
B: uint8(float64(c.B) + (float64(other.B)-float64(c.B))*t),
|
||||
}
|
||||
}
|
||||
|
||||
// RGBBlack is the zero value black color
|
||||
var RGBBlack = RGB{0, 0, 0}
|
||||
|
||||
// 6-bit quantized LUT for Redmean-based 256-color mapping
|
||||
// 64×64×64 = 262,144 bytes, fits in L2 cache
|
||||
var lut256 [64 * 64 * 64]uint8
|
||||
|
||||
func init() {
|
||||
// Pre-compute Redmean-based palette mapping for all 6-bit quantized RGB values
|
||||
for r := 0; r < 64; r++ {
|
||||
for g := 0; g < 64; g++ {
|
||||
for b := 0; b < 64; b++ {
|
||||
// Expand 6-bit to 8-bit (shift left 2, add 2 for midpoint)
|
||||
r8 := (r << 2) | 2
|
||||
g8 := (g << 2) | 2
|
||||
b8 := (b << 2) | 2
|
||||
lut256[r<<12|g<<6|b] = computeRedmean256(r8, g8, b8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// computeRedmean256 finds the nearest 256-palette index using Redmean distance
|
||||
// Called only at init() to populate LUT
|
||||
func computeRedmean256(r, g, b int) uint8 {
|
||||
// Grayscale fast path
|
||||
if r == g && g == b {
|
||||
if r < 8 {
|
||||
return 16
|
||||
}
|
||||
if r > 238 {
|
||||
return 231
|
||||
}
|
||||
return uint8(232 + (r-8)/10)
|
||||
}
|
||||
|
||||
bestIdx := uint8(16)
|
||||
minDist := 1 << 30
|
||||
|
||||
// Search 6×6×6 cube (indices 16-231)
|
||||
for i := 0; i < 216; i++ {
|
||||
cr := cubeValues[i/36]
|
||||
cg := cubeValues[(i/6)%6]
|
||||
cb := cubeValues[i%6]
|
||||
|
||||
d := redmeanDistance(r, g, b, cr, cg, cb)
|
||||
if d < minDist {
|
||||
minDist = d
|
||||
bestIdx = uint8(16 + i)
|
||||
}
|
||||
}
|
||||
|
||||
// Search grayscale ramp (indices 232-255)
|
||||
for i := 0; i < 24; i++ {
|
||||
gray := 8 + i*10
|
||||
d := redmeanDistance(r, g, b, gray, gray, gray)
|
||||
if d < minDist {
|
||||
minDist = d
|
||||
bestIdx = uint8(232 + i)
|
||||
}
|
||||
}
|
||||
|
||||
return bestIdx
|
||||
}
|
||||
|
||||
// redmeanDistance calculates perceptually-weighted color distance
|
||||
// Formula: https://en.wikipedia.org/wiki/Color_difference#sRGB
|
||||
func redmeanDistance(r1, g1, b1, r2, g2, b2 int) int {
|
||||
rmean := (r1 + r2) / 2
|
||||
dr := r1 - r2
|
||||
dg := g1 - g2
|
||||
db := b1 - b2
|
||||
return (((512 + rmean) * dr * dr) >> 8) + 4*dg*dg + (((767 - rmean) * db * db) >> 8)
|
||||
}
|
||||
|
||||
// Color cube values for 6×6×6 palette (indices 16-231)
|
||||
var cubeValues = [6]int{0, 95, 135, 175, 215, 255}
|
||||
|
||||
// RGBTo256 converts RGB to nearest 256-color palette index
|
||||
// O(1) lookup via pre-computed Redmean LUT
|
||||
func RGBTo256(c RGB) uint8 {
|
||||
return lut256[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user