v0.1.2 xterm 256 color addition, examples in cmd dir

This commit is contained in:
2026-07-19 05:43:42 -04:00
parent 49124ee973
commit 615e11bc78
3 changed files with 316 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
package main
import (
"fmt"
"strings"
"github.com/lixenwraith/color"
)
const reset = "\x1b[0m"
// bg256 returns the SGR sequence for an 8-bit xterm-256 background.
// This is the sequence used by naked terminals (TTY, basic SSH).
func bg256(idx uint8) string { return fmt.Sprintf("\x1b[48;5;%dm", idx) }
// bgRGB returns the SGR sequence for a 24-bit truecolor background.
func bgRGB(c color.RGB) string { return fmt.Sprintf("\x1b[48;2;%d;%d;%dm", c.R, c.G, c.B) }
// block256 renders n background-colored spaces using the 256-color palette.
func block256(idx uint8, n int) string { return bg256(idx) + strings.Repeat(" ", n) + reset }
// blockRGB renders n background-colored spaces using truecolor.
func blockRGB(c color.RGB, n int) string { return bgRGB(c) + strings.Repeat(" ", n) + reset }
// =====================================================================
// PART 1: 256 Color Mechanics
// =====================================================================
func show256Cube() {
fmt.Println("── Part 1: xterm-256 Color Cube (6x6x6) ──")
fmt.Println("Displaying the 216-color cube layout (Indices 16-231).")
// The standard xterm-256 cube consists of 6 "slices" of red,
// containing 6x6 grids of green and blue.
for r := uint8(0); r < 6; r++ {
for g := uint8(0); g < 6; g++ {
for b := uint8(0); b < 6; b++ {
// Get exact index using the pure math function
idx := color.Cube256(r, g, b)
fmt.Print(block256(idx, 3))
}
fmt.Print(" ") // Space between green columns
}
fmt.Println()
}
fmt.Println()
}
func showGrayscaleRamp() {
fmt.Println("── Part 1: Grayscale Ramp ──")
fmt.Println("Displaying the 24-step grayscale ramp (Indices 232-255).")
for step := uint8(0); step < 24; step++ {
idx := color.Gray256(step)
fmt.Print(block256(idx, 2))
}
fmt.Println("\n")
}
// =====================================================================
// PART 2: Naked Terminal (No Desktop Environment)
// =====================================================================
func showNakedTerminalDegradation() {
fmt.Println("── Part 2: Naked Terminal (Quantization Fallback) ──")
fmt.Println("Simulating how 24-bit Truecolor gracefully degrades in environments")
fmt.Println("without a modern desktop compositor (e.g., bare TTY, tmux, legacy SSH).")
fmt.Println()
// 1. Force the lazy evaluation of the 262KB Redmean LUT.
// This makes RGBTo256 O(1) during the render loop.
color.WarmXterm256()
// 2. Render a smooth gradient in both truecolor and degraded 256-color
const steps = 40
start, end := color.ElectricViolet, color.LimeGreen
var truecolor strings.Builder
var naked256 strings.Builder
for i := 0; i < steps; i++ {
t := float64(i) / float64(steps-1)
c := color.Lerp(start, end, t)
// Desktop Environment (24-bit)
truecolor.WriteString(blockRGB(c, 2))
// Naked Terminal (8-bit Quantized via Redmean perceptual distance)
idx := color.RGBTo256(c)
naked256.WriteString(block256(idx, 2))
}
fmt.Println("Desktop Environment (24-bit smooth interpolation):")
fmt.Println(truecolor.String())
fmt.Println("Naked Terminal (8-bit perceptual quantization via RGBTo256):")
fmt.Println(naked256.String())
fmt.Println()
// 3. Show exact index mapping for specific named colors
fmt.Println("Nearest xterm-256 mappings for Named RGB colors:")
named := []struct {
name string
c color.RGB
}{
{"HotPink", color.HotPink},
{"BurntOrange", color.BurntOrange},
{"MintGreen", color.MintGreen},
{"DeepNavy", color.DeepNavy},
{"SlateGray", color.SlateGray},
}
for _, n := range named {
// Calculate nearest palette index
idx := color.RGBTo256(n.c)
// Map index back to mathematical coordinates to see where it landed
cubeStr := ""
if idx >= 16 && idx <= 231 {
r, g, b := color.CubeRGB256(idx)
cubeStr = fmt.Sprintf("Cube(r:%d, g:%d, b:%d)", r, g, b)
} else if idx >= 232 {
cubeStr = fmt.Sprintf("GrayStep(%d)", idx-232)
} else {
cubeStr = "System(0-15)"
}
fmt.Printf(" %-12s %s (Truecolor) -> %s (Index %3d) %s\n",
n.name,
blockRGB(n.c, 4),
block256(idx, 4),
idx,
cubeStr,
)
}
fmt.Println()
}
func main() {
// Execute Part 1
show256Cube()
showGrayscaleRamp()
// Execute Part 2
showNakedTerminalDegradation()
}
+171
View File
@@ -0,0 +1,171 @@
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
}