v0.1.2 xterm 256 color addition, examples in cmd dir
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user