package ascii import ( "bytes" "fmt" "image/jpeg" "io" "net/http" "unicode/utf8" ) func NewWriter(w io.Writer, foreground, background, text string) io.Writer { // once clear screen _, _ = w.Write([]byte(csiClear)) // every frame - move to home a := &writer{wr: w, buf: []byte(csiHome)} // https://en.wikipedia.org/wiki/ANSI_escape_code switch foreground { case "": case "8": a.color = func(r, g, b uint8) { idx := xterm256color(r, g, b, 8) a.appendEsc(fmt.Sprintf("\033[%dm", 30+idx)) } case "256": a.color = func(r, g, b uint8) { idx := xterm256color(r, g, b, 255) a.appendEsc(fmt.Sprintf("\033[38;5;%dm", idx)) } case "rgb": a.color = func(r, g, b uint8) { a.appendEsc(fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b)) } default: a.buf = append(a.buf, "\033["+foreground+"m"...) } switch background { case "": case "8": a.color = func(r, g, b uint8) { idx := xterm256color(r, g, b, 8) a.appendEsc(fmt.Sprintf("\033[%dm", 40+idx)) } case "256": a.color = func(r, g, b uint8) { idx := xterm256color(r, g, b, 255) a.appendEsc(fmt.Sprintf("\033[48;5;%dm", idx)) } case "rgb": a.color = func(r, g, b uint8) { a.appendEsc(fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b)) } default: a.buf = append(a.buf, "\033["+background+"m"...) } a.pre = len(a.buf) // save prefix size if len(text) == 1 { // fast 1 symbol version a.text = func(_, _, _ uint32) { a.buf = append(a.buf, text[0]) } } else { switch text { case "": text = ` .::--~~==++**##%%$@` // default for empty text case "block": text = " ░░▒▒▓▓█" // https://en.wikipedia.org/wiki/Block_Elements } if runes := []rune(text); len(runes) != len(text) { k := float32(len(runes)-1) / 255 a.text = func(r, g, b uint32) { i := gray(r, g, b, k) a.buf = utf8.AppendRune(a.buf, runes[i]) } } else { k := float32(len(text)-1) / 255 a.text = func(r, g, b uint32) { i := gray(r, g, b, k) a.buf = append(a.buf, text[i]) } } } return a } type writer struct { wr io.Writer buf []byte pre int esc string color func(r, g, b uint8) text func(r, g, b uint32) } // https://stackoverflow.com/questions/37774983/clearing-the-screen-by-printing-a-character const csiClear = "\033[2J" const csiHome = "\033[H" func (a *writer) Write(p []byte) (n int, err error) { img, err := jpeg.Decode(bytes.NewReader(p)) if err != nil { return 0, err } a.buf = a.buf[:a.pre] // restore prefix w := img.Bounds().Dx() h := img.Bounds().Dy() for y := 0; y < h; y++ { for x := 0; x < w; x++ { r, g, b, _ := img.At(x, y).RGBA() if a.color != nil { a.color(uint8(r>>8), uint8(g>>8), uint8(b>>8)) } a.text(r, g, b) } a.buf = append(a.buf, '\n') } a.appendEsc("\033[0m") if _, err = a.wr.Write(a.buf); err != nil { return 0, err } a.wr.(http.Flusher).Flush() return len(p), nil } // appendEsc - append ESC code to buffer, and skip duplicates func (a *writer) appendEsc(s string) { if a.esc != s { a.esc = s a.buf = append(a.buf, s...) } } func gray(r, g, b uint32, k float32) uint8 { gr := (19595*r + 38470*g + 7471*b + 1<<15) >> 24 // uint8 return uint8(float32(gr) * k) } const x256r = "\x00\x80\x00\x80\x00\x80\x00\xc0\x80\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee" const x256g = "\x00\x00\x80\x80\x00\x00\x80\xc0\x80\x00\xff\xff\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee" const x256b = "\x00\x00\x00\x00\x80\x80\x80\xc0\x80\x00\x00\x00\xff\xff\xff\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee" func xterm256color(r, g, b uint8, n int) (index uint8) { best := uint16(0xFFFF) for i := 0; i < n; i++ { diff := sqDiff(r, x256r[i]) + sqDiff(g, x256g[i]) + sqDiff(b, x256b[i]) if diff < best { best = diff index = uint8(i) } } return } // sqDiff - just like from image/color/color.go func sqDiff(x, y uint8) uint16 { d := uint16(x - y) //return d return (d * d) >> 2 }