package main import ( "fmt" "math" "math/rand" "time" "codeberg.org/historia/xterm256-color-palette/color" "github.com/nsf/termbox-go" ) // обертка над termbox'ом type Screen struct { Buffer []termbox.Cell Width int Height int } func (s *Screen) reScreen() { s.Buffer = termbox.CellBuffer() s.Width, s.Height = termbox.Size() } func (s *Screen) Init() error { err := termbox.Init() if err == nil { s.reScreen() } return err } func (s *Screen) Flush() { termbox.Flush() s.reScreen() } func (s *Screen) Clear(fg, bg termbox.Attribute) { termbox.Clear(fg, bg) s.reScreen() } var screen Screen func termNative(c int) termbox.Attribute { return termbox.Attribute(c + 1) } type hitRegion struct { x, y, w, h int leftAction func(x, y int) rightAction func(x, y int) } var hitRegions []hitRegion func addHitRegion(x, y, w, h int, leftAction, rightAction func(x, y int)) { hitRegions = append(hitRegions, hitRegion{x, y, w, h, leftAction, rightAction}) } func hitTest(px, py int) *hitRegion { for i := len(hitRegions) - 1; i >= 0; i-- { r := &hitRegions[i] if px >= r.x && px < r.x+r.w && py >= r.y && py < r.y+r.h { return r } } return nil } var selectedColors []int var paletteScroll int var autoScrollPalette bool var paletteLeft int var paletteTop int var lastMouseEvent time.Time var dragIndex = -1 var dragY int func addSelectedColor(c int) { for _, sc := range selectedColors { if sc == c { return } } selectedColors = append(selectedColors, c) autoScrollPalette = true } func removeSelectedColor(i int) { if i >= 0 && i < len(selectedColors) { selectedColors = append(selectedColors[:i], selectedColors[i+1:]...) } if paletteScroll > len(selectedColors)-1 { paletteScroll = len(selectedColors) - 1 } if paletteScroll < 0 { paletteScroll = 0 } } func moveSelectedColor(fromIdx, toIdx int) { if fromIdx < 0 || fromIdx >= len(selectedColors) || toIdx < 0 || toIdx >= len(selectedColors) || fromIdx == toIdx { return } c := selectedColors[fromIdx] selectedColors = append(selectedColors[:fromIdx], selectedColors[fromIdx+1:]...) selectedColors = append(selectedColors[:toIdx], append([]int{c}, selectedColors[toIdx:]...)...) } var harmonies = []struct { name string offsets []float64 }{ {"complementary", []float64{0, 180}}, {"analogous", []float64{0, 330, 30}}, {"triadic", []float64{0, 120, 240}}, {"split-comp", []float64{0, 150, 210}}, {"tetradic", []float64{0, 60, 180, 240}}, {"square", []float64{0, 90, 180, 270}}, {"monochromatic", nil}, } var currentHarmony = 2 // triadic // включать ли 16 системных цветов терминала в генерацию палитр // (их реальный вид зависит от темы терминала) var useSystemColors bool // привязка LCh-цвета к палитре xterm с уменьшением насыщенности // до попадания в sRGB-охват (сохраняя светлоту и тон) func snapColor(lch color.LChColor) int { for lch.C > 0.5 && !color.InGamut(lch) { lch.C *= 0.95 } var col int if useSystemColors { _, col = color.ApproximateAll(lch, comparers[currentComparer]) } else { _, col = color.Approximate(lch, comparers[currentComparer]) } return col } // генерация гармоничной палитры в пространстве LCh func generateHarmony() { har := harmonies[currentHarmony] var base color.LChColor seeded := false if len(selectedColors) > 0 { c := selectedColors[0] if useSystemColors || c-1 >= 16 { base = color.TermLab(c).LCh() seeded = true } } if !seeded { rnd := color.LChColor{ L: 40.0 + 40.0*rand.Float64(), C: 40.0 + 50.0*rand.Float64(), H: 360.0 * rand.Float64(), } // привязка к ближайшему цвету xterm, чтобы повторная генерация давала тот же результат c := snapColor(rnd) base = color.TermLab(c).LCh() } selectedColors = selectedColors[:0] paletteScroll = 0 if har.offsets == nil { // монохроматическая: один тон, разная светлота for i := 0; i < 5; i++ { l := 20.0 + 15.0*float64(i) addSelectedColor(snapColor(color.LChColor{L: l, C: base.C, H: base.H})) } } else { for _, off := range har.offsets { hue := math.Mod(base.H+off, 360.0) addSelectedColor(snapColor(color.LChColor{L: base.L, C: base.C, H: hue})) } } } // расширение палитры: для каждого цвета добавляются тень, блик и приглушенный // вариант того же тона, сгруппированные сразу после исходного цвета func expandPalette() { if len(selectedColors) == 0 { return } clampL := func(l float64) float64 { if l < 10.0 { return 10.0 } if l > 90.0 { return 90.0 } return l } expanded := make([]int, 0, len(selectedColors)*4) contains := func(c int) bool { for _, sc := range expanded { if sc == c { return true } } return false } for _, c := range selectedColors { if !contains(c) { expanded = append(expanded, c) } if !useSystemColors && c-1 < 16 { continue } lch := color.TermLab(c).LCh() variants := []color.LChColor{ {L: clampL(lch.L - 25.0), C: lch.C, H: lch.H}, {L: clampL(lch.L + 25.0), C: lch.C, H: lch.H}, {L: lch.L, C: lch.C * 0.5, H: lch.H}, } for _, v := range variants { col := snapColor(v) if !contains(col) { expanded = append(expanded, col) } } } selectedColors = expanded paletteScroll = 0 autoScrollPalette = false } func printString(x, y int, s string) { // TODO срабатывает перенос на следующую строку при достижении правого края // и ошибка при выходе из границ буфера снизу offsetX := 0 offsetY := 0 for _, char := range s { if char == '\n' { offsetX = 0 offsetY++ } else { screen.Buffer[x+offsetX+(y+offsetY)*screen.Width].Ch = char offsetX++ } } } func printLine(x, y int, s string) { printLineAttr(x, y, s, termbox.ColorWhite, background) } func printLineAttr(x, y int, s string, fg, bg termbox.Attribute) { offsetX := 0 offsetY := 0 for _, char := range s { if char == '\n' { offsetX = 0 offsetY++ } else { termbox.SetCell(x+offsetX, y+offsetY, char, fg, bg) offsetX++ } } } func DrawColor(x, y int, c int) { Grayscale := []byte{ 16, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 231, } for xx := 0; xx < 13; xx++ { termbox.SetCell(x+xx, y, ' ', termbox.ColorBlack, termbox.Attribute(c)) } // round(36 * (r * 5) + 6 * (g * 5) + (b * 5) + 16) // https://gist.github.com/MicahElliott/719710 v := int(0.2125*float64(color.XtermRGBPalette[c].R) + 0.7154*float64(color.XtermRGBPalette[c].G) + 0.0721*float64(color.XtermRGBPalette[c].B)) fg := termbox.ColorRed // if v < 128 { // fg = termbox.ColorWhite //fg = termbox.Attribute(231) delta := 64 center := 128 if (v > (center - delta)) && (v <= center) { fg = termbox.Attribute(Grayscale[25]) } else if (v > center) && (v < (center + delta)) { fg = termbox.Attribute(Grayscale[0]) } else { //fg = termbox.Attribute(Grayscale[12]) fg = termbox.Attribute(Grayscale[25-int(v/11)]) } printLineAttr(x+1, y, fmt.Sprintf("%03d #%s", int(c), color.XtermRGBPalette[c]), fg, termbox.Attribute(c)) } var ( boxCacheSize int boxCacheLight int boxCacheComp int boxCacheBlocks []struct{ fg, bg termbox.Attribute } ) func DrawBox(left, top, d, l int) { var approxer string switch currentComparer { case 0: approxer = "CIE76" case 1: approxer = "CIE94" case 2: approxer = "CIE2000" } hit := d == boxCacheSize && l == boxCacheLight && currentComparer == boxCacheComp && boxCacheBlocks != nil if hit { idx := 0 for y := 0; y < d; y++ { for x := 0; x < d; x++ { blk := boxCacheBlocks[idx] termbox.SetCell(x*2+left, y+top, ' ', blk.fg, blk.bg) termbox.SetCell(x*2+left+1, y+top, ' ', blk.fg, blk.bg) idx++ } } } else { r := d / 2 boxCacheBlocks = make([]struct{ fg, bg termbox.Attribute }, d*d) idx := 0 for y := 0; y < d; y++ { for x := 0; x < d; x++ { a := 100.0 * float64(x-r) / float64(r) b := -100.0 * float64(y-r) / float64(r) _, c := color.Approximate(color.LabColor{float64(l), a, b}, comparers[currentComparer]) fg := int(background) if y == 1 || y == 2 || y == d-2 { fgLab := color.TermLab(c) if l > 50 { fgLab.L -= 50 } else { fgLab.L += 50 } _, fg = color.Approximate(fgLab, color.DeltaCIE2000{}) } boxCacheBlocks[idx] = struct{ fg, bg termbox.Attribute }{termbox.Attribute(fg), termbox.Attribute(c)} termbox.SetCell(x*2+left, y+top, ' ', termbox.Attribute(fg), termbox.Attribute(c)) termbox.SetCell(x*2+left+1, y+top, ' ', termbox.Attribute(fg), termbox.Attribute(c)) idx++ } } boxCacheSize, boxCacheLight, boxCacheComp = d, l, currentComparer } labLabel := "CIE L*a*b color space" if len(labLabel)+4 < d*2 { printString(left+2, top+1, labLabel) printString(left+2, top+2, fmt.Sprintf("Lightness = %d%%", l)) } //printString(left+2, top+4, fmt.Sprintln("Press Up/Down key for change")) approxerLabel := fmt.Sprintf("Approximate by %s algorithm", approxer) if len(approxerLabel)+4 <= d*2 { printString(left+2, top+d-2, approxerLabel) } //printString(left+2, top+7, fmt.Sprintln("Press Left/Right key for change")) } func DrawCircle(left, top, r int, l float64) { var ch [2]rune point := func(xb, yb int) { xl := xb / r yl := yb / r distance, color := color.Approximate(color.LChColor{l, math.Sqrt(float64(xl*xl) + float64(yl*yl)), 180.0 * math.Atan2(float64(xl), float64(yl))}, comparers[currentComparer]) //distance, color := color.Palette(color.LabColor{l, float64(xb * 120.0 / r), float64(-yb * 120.0 / r)}, color.DeltaCIE94{}) if distance < 50 { ch[0] = ' ' ch[1] = ' ' } else { //ch[0] = '¤' //ch[1] = '¤' ch[0] = '(' ch[1] = ')' } termbox.SetCell((xb+r)*2+left, yb+r+top, ch[0], termbox.Attribute(0), termbox.Attribute(color)) termbox.SetCell((xb+r)*2+left+1, yb+r+top, ch[1], termbox.Attribute(0), termbox.Attribute(color)) } // алгоритм Брезенхэма line := func(x, y1, y2 int) { for yf := y1; yf <= y2; yf++ { point(x, yf) } } x := 0 y := r d := 3 - 2*r for x <= y { line(x, -y, y) line(-x, -y, y) line(y, -x, x) line(-y, -x, x) if d < 0 { d += 4*x + 6 } else { d += 4*(x-y) + 10 y-- } x++ } } func DrawColorCell(x, y, c int) { termbox.SetCell(x, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) termbox.SetCell(x+1, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) val := c addHitRegion(x, y, 2, 1, func(_, _ int) { addSelectedColor(val) }, nil) } // left, top - левый верхний угол отрисовки палитры // row - количество кубов 6x6 по горизонтали // num - признак отрисовки номеров цветов func DrawPalette(left, top, row int, num bool) { var numOffset int if num { numOffset = 4 } //bh := [2]termbox.Attribute{237, background} bh := [2]termbox.Attribute{236, background} //th := [2]termbox.Attribute{232, 241} th := [2]termbox.Attribute{243, 243} cc := 1 printLineAttr(left, top, fmt.Sprintln("16 system colors"), text, background) printLineAttr(left, top+2, fmt.Sprintf("%3d ", cc-1), th[1], bh[1]) printLineAttr(left+numOffset+16*2, top+2, fmt.Sprintf(" %-3d", cc+14), th[1], bh[1]) for x := 0; x < 16; x++ { DrawColorCell(left+numOffset+x*2, top+2, cc) cc++ } padding := 1 if num { // padding = 0 } printLineAttr(left, top+4, fmt.Sprintln("216 colors of color cube 6x6x6"), text, background) col := int(6 / row) // ширина for l := 0; l < row; l++ { // l - вертикальный счетчик for k := 0; k < col; k++ { // k - горизонтальный счетчик for y := 0; y < 6; y++ { h := y % 2 if h != 0 { h = 1 } printLineAttr(left+k*numOffset*2+k*(6+padding)*2, top+l*(6+padding)+y+6, fmt.Sprintf("%3d ", cc-1), th[h], bh[h]) printLineAttr(left+k*numOffset*2+k*(6+padding)*2+16, top+l*(6+padding)+y+6, fmt.Sprintf(" %-3d", cc+4), th[h], bh[h]) for x := 0; x < 6; x++ { DrawColorCell(left+numOffset+k*(6+padding)*2+k*numOffset*2+x*2, top+l*(6+padding)+y+6, cc) cc++ } } } } printLineAttr(left, top+row*7+6, fmt.Sprintln("24 grayscale colors"), text, background) for y := 0; y < 2; y++ { for x := 0; x < 12; x++ { DrawColorCell(left+numOffset+x*2, top+row*7+y+8, cc) cc++ } } printLineAttr(left, top+row*7+8, fmt.Sprintf("%3d ", 232), th[1], bh[1]) printLineAttr(left+28, top+row*7+8, fmt.Sprintf(" %-3d", 243), th[1], bh[1]) printLineAttr(left, top+row*7+9, fmt.Sprintf("%3d ", 244), th[1], bh[1]) printLineAttr(left+28, top+row*7+9, fmt.Sprintf(" %-3d ", 255), th[1], bh[1]) } func Fill(x1, y1, x2, y2 int, fg, bg termbox.Attribute) { for y := y1; y <= y2; y++ { for x := x1; x <= x2; x++ { termbox.SetCell(x, y, ' ', fg, bg) } } } func drawPaletteEntry(x, y, c, idx int) { termbox.SetCell(x, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) termbox.SetCell(x+1, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) label := fmt.Sprintf("%03d / %02X", c-1, c-1) if dragIndex == idx { printLineAttr(x+3, y, label, background, background+7) } else { printLineAttr(x+3, y, label, text, background) } addHitRegion(x, y, 3+len(label), 1, func(_, _ int) { if dragIndex < 0 { dragIndex = idx } }, func(_, _ int) { removeSelectedColor(idx) if dragIndex == idx { dragIndex = -1 } else if dragIndex > idx { dragIndex-- } }) } // панель пользовательской палитры func DrawUserPalette(left, top int) { paletteLeft = left paletteTop = top printLineAttr(left, top, fmt.Sprintf("Palette (%d) [%s]", len(selectedColors), harmonies[currentHarmony].name), text, background) n := len(selectedColors) if n == 0 { paletteScroll = 0 autoScrollPalette = false printLineAttr(left, top+2, "Click a color to add it here,", background+6, background) printLineAttr(left, top+3, "or press Space to generate", background+6, background) return } rows := screen.Height - (top + 2) if rows <= 0 { autoScrollPalette = false return } if n <= rows { paletteScroll = 0 autoScrollPalette = false for i, c := range selectedColors { drawPaletteEntry(left, top+2+i, c, i) } return } maxScroll := n - (rows - 1) if maxScroll > n-1 { maxScroll = n - 1 } if autoScrollPalette { paletteScroll = maxScroll autoScrollPalette = false } if paletteScroll > maxScroll { paletteScroll = maxScroll } if paletteScroll < 0 { paletteScroll = 0 } visible := rows if paletteScroll > 0 { visible-- } if paletteScroll+visible < n { visible-- } if visible < 1 { visible = 1 } y := top + 2 if paletteScroll > 0 { printLineAttr(left, y, fmt.Sprintf("^ %d more", paletteScroll), background+7, background) y++ } end := paletteScroll + visible if end > n { end = n } for i := paletteScroll; i < end; i++ { drawPaletteEntry(left, y, selectedColors[i], i) y++ } if below := n - end; below > 0 && y < screen.Height { printLineAttr(left, y, fmt.Sprintf("v %d more", below), background+7, background) } } var ( text, background termbox.Attribute lightness int helpVisible bool ) func DrawChart() { hitRegions = hitRegions[:0] screen.Clear(text, background) //Fill(0, 0, screen.Width-1, screen.Height-1, text, background) Fill(0, 0, screen.Width-1, 0, background, background+8) printString(2, 0, "XTerm 256 color palette chart") printString(screen.Width-19, 0, "F1 Help F10 Exit") printLineAttr(screen.Width-19, 0, "F1", background+20, background+8) printLineAttr(screen.Width-10, 0, "F10", background+20, background+8) paletteSize := 42 boxSize := screen.Height - 3 w := int((screen.Width - paletteSize - 6) / 2) if w < boxSize { boxSize = w } DrawBox(2, 2, boxSize, lightness) // http://snag.gy/XMro8.jpg - картинка для сравнения boxLeft, boxTop := 2, 2 boxW, boxH := boxSize*2, boxSize bs := boxSize lt := lightness addHitRegion(boxLeft, boxTop, boxW, boxH, func(px, py int) { cx := (px - boxLeft) / 2 cy := py - boxTop r := bs / 2 if r > 0 && cx >= 0 && cx < bs && cy >= 0 && cy < bs { a := 100.0 * float64(cx-r) / float64(r) b := -100.0 * float64(cy-r) / float64(r) _, col := color.Approximate(color.LabColor{float64(lt), a, b}, comparers[currentComparer]) addSelectedColor(col) } }, nil) sysState := "off" if useSystemColors { sysState = "on" } helpText := []string{ "Control keys:", "", "F1 - show/hide this help", "Up/Down - change lightness", "Left/Right - change approximation method", "", "CIE76 - fastest", "CIE94 - middle", "CIE2000 - slowest", "", "Click a color to add it to the palette", "L-click palette entry to pick up / drop", "R-click palette entry to remove", "Backspace - remove last palette entry", "Space - generate harmony palette", " (seeds from first entry if any)", "h - change harmony mode", "e - expand palette with similar colors", "t - use 16 system colors in generated", fmt.Sprintf(" palettes, as seeds and outputs (%s).", sysState), " When off, a system color seed is", " replaced by a random one. Note: system", " colors depend on your terminal theme,", " so results assume xterm defaults", "q/Esc/F10 - quit", "Wheel/PgUp/PgDn - scroll palette", "", } xc := boxSize*2 + 4 if xc < 2 { xc = 2 } yc := 2 if helpVisible { for i, line := range helpText { printString(xc, yc+i, line) } } else { DrawPalette(xc, yc, 3, true) } DrawUserPalette(xc, yc+3*7+11) screen.Flush() } var comparers []color.Comparer var currentComparer int func main() { err := screen.Init() if err != nil { panic(err) } defer termbox.Close() termbox.SetOutputMode(termbox.Output256) termbox.SetInputMode(termbox.InputEsc | termbox.InputMouse) background = termNative(234) text = background + 12 lightness = 100 comparers = append(comparers, color.DeltaCIE76{}, color.DeltaCIE94{}, color.DeltaCIE2000{}) //size := int(h/2 - 3) //DrawCircle(2, 1, size, 1.0) DrawChart() for loop := true; loop; { switch ev := termbox.PollEvent(); ev.Type { case termbox.EventKey: switch ev.Key { case termbox.KeyCtrlQ, termbox.KeyF10: loop = false case termbox.KeyEsc: if time.Since(lastMouseEvent) > 150*time.Millisecond { loop = false } case termbox.KeyBackspace, termbox.KeyBackspace2: if len(selectedColors) > 0 { removeSelectedColor(len(selectedColors) - 1) DrawChart() } case termbox.KeyF1: helpVisible = !helpVisible DrawChart() case termbox.KeyArrowUp: if lightness < 100 { lightness = lightness + 10.0 DrawChart() } case termbox.KeyArrowDown: if lightness > 0 { lightness = lightness - 10.0 DrawChart() } case termbox.KeyArrowLeft: if currentComparer > 0 { currentComparer-- } else { currentComparer = len(comparers) - 1 } DrawChart() case termbox.KeyArrowRight: if currentComparer < 2 { currentComparer++ } else { currentComparer = 0 } DrawChart() case termbox.KeySpace: generateHarmony() DrawChart() case termbox.KeyPgup: if paletteScroll > 0 { paletteScroll-- DrawChart() } case termbox.KeyPgdn: paletteScroll++ DrawChart() default: switch ev.Ch { case 'q', 'Q': if time.Since(lastMouseEvent) > 150*time.Millisecond { loop = false } case 'h', 'H': currentHarmony = (currentHarmony + 1) % len(harmonies) DrawChart() case 'e', 'E': expandPalette() DrawChart() case 't', 'T': useSystemColors = !useSystemColors DrawChart() } } case termbox.EventMouse: lastMouseEvent = time.Now() switch ev.Key { case termbox.MouseLeft: if dragIndex < 0 { if r := hitTest(ev.MouseX, ev.MouseY); r != nil && r.leftAction != nil { r.leftAction(ev.MouseX, ev.MouseY) } } dragY = ev.MouseY if dragIndex >= 0 { target := func() int { n := len(selectedColors) rows := screen.Height - (paletteTop + 2) if n <= rows { t := dragY - (paletteTop + 2) if t < 0 { t = 0 } if t >= n { t = n - 1 } return t } firstEntryY := paletteTop + 2 if paletteScroll > 0 { firstEntryY++ } t := paletteScroll + (dragY - firstEntryY) if t < 0 { t = 0 } if t >= n { t = n - 1 } return t }() if target != dragIndex { moveSelectedColor(dragIndex, target) dragIndex = target } } DrawChart() case termbox.MouseRight: if r := hitTest(ev.MouseX, ev.MouseY); r != nil && r.rightAction != nil { r.rightAction(ev.MouseX, ev.MouseY) DrawChart() } case termbox.MouseRelease: if dragIndex >= 0 { dragIndex = -1 DrawChart() } case termbox.MouseWheelUp: if ev.MouseX >= paletteLeft && paletteScroll > 0 { paletteScroll-- DrawChart() } case termbox.MouseWheelDown: if ev.MouseX >= paletteLeft && len(selectedColors) > 0 { paletteScroll++ DrawChart() } } case termbox.EventResize: DrawChart() } } }