Skip to content

Commit

Permalink
(Monochrome but double vertical resolution) Picture loading and demo …
Browse files Browse the repository at this point in the history
…of it into fps (#29)

* Ability to load monochrome images, add -image (and a default one) in background

* smaller/better default picture

* Update to build fps binary from merged workflows, readme update (#28)

* Trying to figure out why it doesn't build on tags even

* wasn't triggering on tags

* avoid dirty state

* missing Dockerfile for fps

* switch to released workflow, add readme

* fix releases link

* Update the sshot

* linter

* Don't clear eol
  • Loading branch information
ldemailly authored Sep 23, 2024
1 parent 8bd0e97 commit 6d51f54
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 6 deletions.
1 change: 1 addition & 0 deletions ansipixels/ansipixels.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// ansipixel provides terminal drawing and key reading abilities. fps/fps.go is an example of how to use it.
package ansipixels

import (
Expand Down
35 changes: 29 additions & 6 deletions ansipixels/fps/fps.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package main

import (
"bytes"
_ "embed"
"flag"
"fmt"
"image"
"os"
"strconv"
"time"
Expand Down Expand Up @@ -77,7 +80,11 @@ func animate(ap *ansipixels.AnsiPixels, frame uint) {
charAt(ap, pos-1, w, h, "\033[0m ") // erase and reset color
}

//go:embed fps.jpg
var fpsJpg []byte

func Main() int {
imgFlag := flag.String("image", "", "Image file to display in monochrome in the background instead of the default one")
cli.MinArgs = 0
cli.MaxArgs = 1
cli.ArgsHelp = "[maxfps]"
Expand Down Expand Up @@ -112,14 +119,27 @@ func Main() int {
return log.FErrf("Error getting terminal size: %v", err)
}
ap.ClearScreen()
var background *image.RGBA
var err error
if *imgFlag == "" {
background, err = ap.DecodeImage(bytes.NewReader(fpsJpg))
} else {
background, err = ap.ReadImage(*imgFlag)
}
if err != nil {
return log.FErrf("Error reading image: %v", err)
}
if err = ap.ShowImage(background, "\033[34m"); err != nil {
return log.FErrf("Error showing image: %v", err)
}
drawBox(ap)
// FPS test
fps := 0.0
buf := [256]byte{}
// sleep := 1 * time.Second / time.Duration(fps)
ap.WriteCentered(ap.H/2+3, "FPS %s test... any key to start; q, ^C, or ^D to exit... ", fpsStr)
ap.Out.Flush()
_, err := ap.In.Read(buf[:])
_, err = ap.In.Read(buf[:])
if err != nil {
return log.FErrf("Error reading key: %v", err)
}
Expand All @@ -146,6 +166,7 @@ func Main() int {
if ap.IsResizeSignal(s) {
_ = ap.GetSize()
ap.ClearScreen()
_ = ap.ShowImage(background, "\033[34m")
drawBox(ap)
continue
}
Expand All @@ -154,13 +175,15 @@ func Main() int {
elapsed = time.Since(now)
fps = 1. / elapsed.Seconds()
now = time.Now()
ap.WriteAt(ap.W/2-20, ap.H/2, "Last frame %v FPS: %.0f Avg %.2f",
ap.WriteAt(ap.W/2-20, ap.H/2, " Last frame %v FPS: %.0f Avg %.2f ",
elapsed.Round(10*time.Microsecond), fps, float64(frames)/now.Sub(startTime).Seconds())
// Request cursor position (note that FPS is about the same without it, the Flush seems to be enough)
ap.ClearEndOfLine()
ap.MoveHorizontally(ap.W - 1)
_, _ = ap.Out.WriteString(ansipixels.Vertical)
/*
ap.ClearEndOfLine()
ap.MoveHorizontally(ap.W - 1)
_, _ = ap.Out.WriteString(ansipixels.Vertical)
*/
animate(ap, frames)
// Request cursor position (note that FPS is about the same without it, the Flush seems to be enough)
_, _, err = ap.ReadCursorPos()
if err != nil {
return log.FErrf("Error with cursor position request: %v", err)
Expand Down
Binary file added ansipixels/fps/fps.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions ansipixels/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package ansipixels

import (
"image"
"image/color"
_ "image/gif" // Import GIF decoder
_ "image/jpeg" // Import JPEG decoder
_ "image/png" // Import PNG decoder
"io"
"os"

"fortio.org/log"
"golang.org/x/image/draw"
)

type Image struct {
Width int
Height int
Data []byte
}

const (
FullPixel = '█'
TopHalfPixel = '▀'
BottomHalfPixel = '▄'
)

func (ap *AnsiPixels) DrawImage(sx, sy int, img *image.Gray, color string) error {
ap.WriteAtStr(sx, sy, color)
var err error
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y += 2 {
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
pixel1 := img.GrayAt(x, y).Y > 127
pixel2 := img.GrayAt(x, y+1).Y > 127
switch {
case pixel1 && pixel2:
_, _ = ap.Out.WriteRune(FullPixel)
case pixel1 && !pixel2:
_, _ = ap.Out.WriteRune(TopHalfPixel)
case !pixel1 && pixel2:
_, _ = ap.Out.WriteRune(BottomHalfPixel)
case !pixel1 && !pixel2:
_ = ap.Out.WriteByte(' ')
}
}
sy++
ap.MoveCursor(sx, sy)
}
_, err = ap.Out.WriteString("\033[0m") // reset color
return err
}

func grayScaleImage(rgbaImg *image.RGBA) *image.Gray {
grayImg := image.NewGray(rgbaImg.Bounds())
// Iterate through the pixels of the NRGBA image and convert to grayscale
for y := rgbaImg.Bounds().Min.Y; y < rgbaImg.Bounds().Max.Y; y++ {
for x := rgbaImg.Bounds().Min.X; x < rgbaImg.Bounds().Max.X; x++ {
rgbaColor := rgbaImg.RGBAAt(x, y)

// Convert to grayscale using the luminance formula
grayValue := uint8(0.299*float64(rgbaColor.R) + 0.587*float64(rgbaColor.G) + 0.114*float64(rgbaColor.B))

// Set the gray value in the destination Gray image
grayImg.SetGray(x, y, color.Gray{Y: grayValue})
}
}
return grayImg
}

func resizeAndCenter(img *image.Gray, maxW, maxH int) *image.Gray {
// Get original image dimensions
origBounds := img.Bounds()
origW := origBounds.Dx()
origH := origBounds.Dy()

// Calculate aspect ratio scaling
scaleW := float64(maxW) / float64(origW)
scaleH := float64(maxH) / float64(origH)
scale := min(scaleW, scaleH) // Choose the smallest scale to fit within bounds

// Calculate new dimensions while preserving aspect ratio
newW := int(float64(origW) * scale)
newH := int(float64(origH) * scale)

canvas := image.NewGray(image.Rect(0, 0, maxW, maxH)) // transparent background (aka black for ANSI)

// Calculate the offset to center the image
offsetX := (maxW - newW) / 2
offsetY := (maxH - newH) / 2

// Resize the image
resized := image.NewGray(image.Rect(0, 0, newW, newH))
draw.CatmullRom.Scale(resized, resized.Bounds(), img, origBounds, draw.Over, nil)
draw.Draw(canvas, image.Rect(offsetX, offsetY, offsetX+newW, offsetY+newH), resized, image.Point{}, draw.Over)
return canvas
}

func convertToRGBA(src image.Image) *image.RGBA {
if rgba, ok := src.(*image.RGBA); ok {
return rgba
}
bounds := src.Bounds()
dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
return dst
}

func (ap *AnsiPixels) ReadImage(path string) (*image.RGBA, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return ap.DecodeImage(file)
}

func (ap *AnsiPixels) DecodeImage(data io.Reader) (*image.RGBA, error) {
// Automatically detect and decode the image format
img, format, err := image.Decode(data)
if err != nil {
return nil, err
}
log.Debugf("Image format: %s", format)
return convertToRGBA(img), nil
}

func (ap *AnsiPixels) ShowImage(imgRGBA *image.RGBA, colorString string) error {
err := ap.GetSize()
if err != nil {
return err
}
return ap.DrawImage(1, 1, resizeAndCenter(grayScaleImage(imgRGBA), ap.W-2, 2*ap.H-2), colorString)
}
Binary file modified fps_sshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
fortio.org/log v1.16.0
fortio.org/safecast v1.0.0
fortio.org/term v0.23.0-fortio-6
golang.org/x/image v0.20.0
golang.org/x/sys v0.25.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRcl
github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240916204253-42ee18b96377 h1:aDWu69N3Si4isYMY1ppnuoGEFypX/E5l4MWA//GPClw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240916204253-42ee18b96377/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

0 comments on commit 6d51f54

Please sign in to comment.