prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

commit ca1c87a822f3f7b138b8b7dd8df4200e478de77f
parent 8f5eaf7d60618af21903edca4a4a78383b94895c
Author: Damian Myrda <damian@prime8.dev>
Date:   Thu, 22 Oct 2026 22:16:00 +0000

web: half-block truecolor portrait in the terminal

Diffstat:
Mgo.mod | 4++--
Minternal/utils/logo.go | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 55 insertions(+), 15 deletions(-)

diff --git a/go.mod b/go.mod @@ -17,6 +17,8 @@ require ( github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894 github.com/charmbracelet/wish v1.4.7 github.com/charmbracelet/x/ansi v0.10.1 + github.com/disintegration/imaging v1.6.2 + github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 github.com/evanw/esbuild v0.27.4 github.com/joho/godotenv v1.5.1 github.com/muesli/termenv v0.16.0 @@ -41,9 +43,7 @@ require ( github.com/charmbracelet/x/windows v0.2.0 // indirect github.com/creack/pty v1.1.21 // indirect github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect - github.com/disintegration/imaging v1.6.2 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.9.0 // indirect github.com/fogleman/gg v1.3.0 // indirect diff --git a/internal/utils/logo.go b/internal/utils/logo.go @@ -1,35 +1,75 @@ package utils import ( + "fmt" + "image" "path/filepath" "strings" "sync" aic "github.com/TheZoraiz/ascii-image-converter/aic_package" + "github.com/disintegration/imaging" ) var logoMu sync.Mutex -// LogoASCII renders static/me.jpg as ascii art. aic refuses color when the -// server's env lacks COLORTERM, so it falls back to plain ascii. -func LogoASCII(width int, colored bool) string { - flags := aic.DefaultFlags() - flags.Width = width - flags.Colored = colored - flags.Dither = true - flags.Threshold = 160 +const logoPath = "static/me.jpg" +// logoCrop frames the face: fractions of the source image (x0, y0, x1, y1). +var logoCrop = [4]float64{0.12, 0.08, 0.88, 0.84} + +// LogoASCII renders static/me.jpg for terminals. Colored output uses +// half-block cells (upper half = foreground, lower half = background), so each +// cell carries two truecolor pixels and the portrait reads as a photo. Plain +// output falls back to ascii art. +func LogoASCII(width int, colored bool) string { logoMu.Lock() defer logoMu.Unlock() - path := filepath.Join("static", "me.jpg") - ascii, err := aic.Convert(path, flags) - if err != nil && colored { - flags.Colored = false - ascii, err = aic.Convert(path, flags) + if colored { + if s, err := halfBlocks(logoPath, width); err == nil { + return s + } } + flags := aic.DefaultFlags() + flags.Width = width + flags.Dither = true + flags.Threshold = 160 + ascii, err := aic.Convert(filepath.FromSlash(logoPath), flags) if err != nil { return "" } return strings.TrimRight(ascii, "\n") } + +func halfBlocks(path string, width int) (string, error) { + src, err := imaging.Open(path, imaging.AutoOrientation(true)) + if err != nil { + return "", err + } + b := src.Bounds() + w, h := float64(b.Dx()), float64(b.Dy()) + rect := image.Rect( + b.Min.X+int(logoCrop[0]*w), b.Min.Y+int(logoCrop[1]*h), + b.Min.X+int(logoCrop[2]*w), b.Min.Y+int(logoCrop[3]*h), + ) + crop := imaging.Crop(src, rect) + // one cell is roughly twice as tall as wide, so two pixel rows per cell + // keep the pixels square + rows := int(float64(width)*float64(rect.Dy())/float64(rect.Dx())+0.5) &^ 1 + img := imaging.Resize(crop, width, rows, imaging.Lanczos) + + var sb strings.Builder + for y := 0; y < rows; y += 2 { + if y > 0 { + sb.WriteByte('\n') + } + for x := 0; x < width; x++ { + t := img.NRGBAAt(x, y) + u := img.NRGBAAt(x, y+1) + fmt.Fprintf(&sb, "\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dmâ–€", t.R, t.G, t.B, u.R, u.G, u.B) + } + sb.WriteString("\x1b[0m") + } + return sb.String(), nil +}