From 0819f877501917a5472aeda7a161b526d20fa390 Mon Sep 17 00:00:00 2001 From: Caleb Bassi Date: Thu, 24 Jan 2019 08:42:13 -0800 Subject: [PATCH] Cleanup image widget --- README.md | 1 + {_example => _examples}/image.go | 58 +++++----- image.go | 191 ------------------------------- symbols.go | 14 ++- widgets/image.go | 186 ++++++++++++++++++++++++++++++ widgets/piechart.go | 2 +- widgets/sparkline.go | 2 +- 7 files changed, 229 insertions(+), 225 deletions(-) rename {_example => _examples}/image.go (81%) delete mode 100644 image.go create mode 100644 widgets/image.go diff --git a/README.md b/README.md index fcd4563..a9de1e7 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ If you upgrade and notice something is missing or don't like a change, revert th - [BarChart](./_examples/barchart.go) - [Canvas](./_examples/canvas.go) - [Gauge](./_examples/gauge.go) +- [Image](./_examples/image.go) - [List](./_examples/list.go) - [Paragraph](./_examples/paragraph.go) - [PieChart](./_examples/piechart.go) diff --git a/_example/image.go b/_examples/image.go similarity index 81% rename from _example/image.go rename to _examples/image.go index 46a924d..fc5c152 100644 --- a/_example/image.go +++ b/_examples/image.go @@ -13,11 +13,13 @@ import ( _ "image/gif" _ "image/jpeg" _ "image/png" + "log" "net/http" "os" "strings" - "github.com/gizak/termui" + ui "github.com/gizak/termui" + "github.com/gizak/termui/widgets" ) func main() { @@ -41,53 +43,53 @@ func main() { images = append(images, im) } - err := termui.Init() - if err != nil { - panic(err) + if err := ui.Init(); err != nil { + log.Fatalf("failed to initialize termui: %v", err) } - defer termui.Close() + defer ui.Close() - img := termui.NewImage(nil) + img := widgets.NewImage(nil) + img.SetRect(0, 0, 50, 50) index := 0 render := func() { img.Image = images[index] if !img.Monochrome { - img.BorderLabel = fmt.Sprintf("Color %d/%d", index+1, len(images)) + img.Title = fmt.Sprintf("Color %d/%d", index+1, len(images)) } else if !img.MonochromeInvert { - img.BorderLabel = fmt.Sprintf("Monochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images)) + img.Title = fmt.Sprintf("Monochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images)) } else { - img.BorderLabel = fmt.Sprintf("InverseMonochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images)) + img.Title = fmt.Sprintf("InverseMonochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images)) } - termui.Render(img) + ui.Render(img) } render() - termui.Handle("/sys/kbd", func(e termui.Event) { - switch e.Data.(termui.EvtKbd).KeyStr { - case "q": - termui.StopLoop() - case "": - index = (index + 1) % len(images) - render() - case "": + uiEvents := ui.PollEvents() + for { + e := <-uiEvents + switch e.ID { + case "q", "": + return + case "": index = (index + len(images) - 1) % len(images) render() - case "": + case "": + index = (index + 1) % len(images) + render() + case "": + img.MonochromeThreshold++ + render() + case "": + img.MonochromeThreshold-- + render() + case "": img.Monochrome = !img.Monochrome render() case "\\": img.MonochromeInvert = !img.MonochromeInvert render() - case "": - img.MonochromeThreshold++ - render() - case "": - img.MonochromeThreshold-- - render() } - }) - - termui.Loop() + } } const gopher = `iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDsJkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAAILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36IMRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZmrSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhktrRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEjyy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5feA+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fUyLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2SnssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg==` diff --git a/image.go b/image.go deleted file mode 100644 index 45abe7a..0000000 --- a/image.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2017 Zack Guo . All rights reserved. -// Use of this source code is governed by a MIT license that can -// be found in the LICENSE file. - -package termui - -import ( - "image" - "image/color" -) - -// Image is an image widget. -type Image struct { - Block - Image image.Image - Monochrome bool - MonochromeThreshold uint8 - MonochromeInvert bool -} - -// NewImage returns a new image widget. -func NewImage(img image.Image) *Image { - im := &Image{ - Block: *NewBlock(), - MonochromeThreshold: 128, - Image: img, - } - im.Width = 64 - im.Height = 48 - return im -} - -// Image implements Bufferer. -func (im *Image) Buffer() Buffer { - buf := im.Block.Buffer() - bufWidth := im.innerArea.Dx() - bufHeight := im.innerArea.Dy() - for bx := 0; bx < bufWidth; bx++ { - for by := 0; by < bufHeight; by++ { - buf.Set(im.innerArea.Min.X+bx, im.innerArea.Min.Y+by, Cell{ - Ch: ' ', - Fg: ColorDefault, - Bg: ColorDefault, - }) - } - } - if im.Image == nil { - return buf - } - imageWidth := im.Image.Bounds().Dx() - imageHeight := im.Image.Bounds().Dy() - if im.Monochrome { - if bufWidth > imageWidth/2 { - bufWidth = imageWidth / 2 - } - if bufHeight > imageHeight/2 { - bufHeight = imageHeight / 2 - } - for bx := 0; bx < bufWidth; bx++ { - for by := 0; by < bufHeight; by++ { - ul := im.colorAverage(2*bx*imageWidth/bufWidth/2, (2*bx+1)*imageWidth/bufWidth/2, 2*by*imageHeight/bufHeight/2, (2*by+1)*imageHeight/bufHeight/2) - ur := im.colorAverage((2*bx+1)*imageWidth/bufWidth/2, (2*bx+2)*imageWidth/bufWidth/2, 2*by*imageHeight/bufHeight/2, (2*by+1)*imageHeight/bufHeight/2) - ll := im.colorAverage(2*bx*imageWidth/bufWidth/2, (2*bx+1)*imageWidth/bufWidth/2, (2*by+1)*imageHeight/bufHeight/2, (2*by+2)*imageHeight/bufHeight/2) - lr := im.colorAverage((2*bx+1)*imageWidth/bufWidth/2, (2*bx+2)*imageWidth/bufWidth/2, (2*by+1)*imageHeight/bufHeight/2, (2*by+2)*imageHeight/bufHeight/2) - buf.Set(im.innerArea.Min.X+bx, im.innerArea.Min.Y+by, Cell{ - Ch: blocksChar(ul, ur, ll, lr, im.MonochromeThreshold, im.MonochromeInvert), - }) - } - } - } else { - if bufWidth > imageWidth { - bufWidth = imageWidth - } - if bufHeight > imageHeight { - bufHeight = imageHeight - } - for bx := 0; bx < bufWidth; bx++ { - for by := 0; by < bufHeight; by++ { - c := im.colorAverage(bx*imageWidth/bufWidth, (bx+1)*imageWidth/bufWidth, by*imageHeight/bufHeight, (by+1)*imageHeight/bufHeight) - buf.Set(im.innerArea.Min.X+bx, im.innerArea.Min.Y+by, Cell{ - Ch: c.ch(), - Fg: c.fgColor(), - Bg: ColorBlack, - }) - } - } - } - return buf -} - -func (im *Image) colorAverage(x0, x1, y0, y1 int) colorAverager { - var c colorAverager - for x := x0; x < x1; x++ { - for y := y0; y < y1; y++ { - c = c.add(im.Image.At(x+im.Image.Bounds().Min.X, y+im.Image.Bounds().Min.Y)) - } - } - return c -} - -type colorAverager struct { - rsum, gsum, bsum, asum, count uint64 -} - -func (c colorAverager) add(col color.Color) colorAverager { - r, g, b, a := col.RGBA() - return colorAverager{ - rsum: c.rsum + uint64(r), - gsum: c.gsum + uint64(g), - bsum: c.bsum + uint64(b), - asum: c.asum + uint64(a), - count: c.count + 1, - } -} - -func (c colorAverager) RGBA() (uint32, uint32, uint32, uint32) { - if c.count == 0 { - return 0, 0, 0, 0 - } else { - return uint32(c.rsum/c.count) & 0xffff, - uint32(c.gsum/c.count) & 0xffff, - uint32(c.bsum/c.count) & 0xffff, - uint32(c.asum/c.count) & 0xffff - } -} - -func (c colorAverager) fgColor() Attribute { - return palette.Convert(c).(paletteColor).attribute -} - -func (c colorAverager) ch() rune { - gray := color.GrayModel.Convert(c).(color.Gray).Y - switch { - case gray < 51: - return ' ' - case gray < 102: - return '░' - case gray < 153: - return '▒' - case gray < 204: - return '▓' - default: - return '█' - } -} - -func (c colorAverager) monochrome(threshold uint8, invert bool) bool { - return c.count != 0 && (color.GrayModel.Convert(c).(color.Gray).Y < threshold != invert) -} - -type paletteColor struct { - rgba color.RGBA - attribute Attribute -} - -func (c paletteColor) RGBA() (uint32, uint32, uint32, uint32) { - return c.rgba.RGBA() -} - -var palette = color.Palette([]color.Color{ - paletteColor{color.RGBA{0, 0, 0, 255}, ColorBlack}, - paletteColor{color.RGBA{255, 0, 0, 255}, ColorRed}, - paletteColor{color.RGBA{0, 255, 0, 255}, ColorGreen}, - paletteColor{color.RGBA{255, 255, 0, 255}, ColorYellow}, - paletteColor{color.RGBA{0, 0, 255, 255}, ColorBlue}, - paletteColor{color.RGBA{255, 0, 255, 255}, ColorMagenta}, - paletteColor{color.RGBA{0, 255, 255, 255}, ColorCyan}, - paletteColor{color.RGBA{255, 255, 255, 255}, ColorWhite}, -}) - -var blocks = [...]rune{ - ' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', - '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█', -} - -func blocksChar(ul, ur, ll, lr colorAverager, threshold uint8, invert bool) rune { - index := 0 - if ul.monochrome(threshold, invert) { - index |= 1 - } - if ur.monochrome(threshold, invert) { - index |= 2 - } - if ll.monochrome(threshold, invert) { - index |= 4 - } - if lr.monochrome(threshold, invert) { - index |= 8 - } - return blocks[index] -} diff --git a/symbols.go b/symbols.go index 11542b3..8f2a74c 100644 --- a/symbols.go +++ b/symbols.go @@ -1,13 +1,19 @@ package termui const ( - SHADED_BLOCK = '░' - DOT = '•' - DOTS = '…' + DOT = '•' + DOTS = '…' ) var ( - BARS = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} + BARS = [...]rune{' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} + + SHADED_BLOCKS = [...]rune{' ', '░', '▒', '▓', '█'} + + IRREGULAR_BLOCKS = [...]rune{ + ' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', + '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█', + } BRAILLE_OFFSET = '\u2800' BRAILLE = [4][2]rune{ diff --git a/widgets/image.go b/widgets/image.go new file mode 100644 index 0000000..aabc160 --- /dev/null +++ b/widgets/image.go @@ -0,0 +1,186 @@ +// Copyright 2017 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package widgets + +import ( + "image" + "image/color" + + . "github.com/gizak/termui" +) + +type Image struct { + Block + Image image.Image + Monochrome bool + MonochromeThreshold uint8 + MonochromeInvert bool +} + +func NewImage(img image.Image) *Image { + return &Image{ + Block: *NewBlock(), + MonochromeThreshold: 128, + Image: img, + } +} + +func (self *Image) Draw(buf *Buffer) { + self.Block.Draw(buf) + + if self.Image == nil { + return + } + + bufWidth := self.Inner.Dx() + bufHeight := self.Inner.Dy() + imageWidth := self.Image.Bounds().Dx() + imageHeight := self.Image.Bounds().Dy() + + if self.Monochrome { + if bufWidth > imageWidth/2 { + bufWidth = imageWidth / 2 + } + if bufHeight > imageHeight/2 { + bufHeight = imageHeight / 2 + } + for bx := 0; bx < bufWidth; bx++ { + for by := 0; by < bufHeight; by++ { + ul := self.colorAverage(2*bx*imageWidth/bufWidth/2, (2*bx+1)*imageWidth/bufWidth/2, 2*by*imageHeight/bufHeight/2, (2*by+1)*imageHeight/bufHeight/2) + ur := self.colorAverage((2*bx+1)*imageWidth/bufWidth/2, (2*bx+2)*imageWidth/bufWidth/2, 2*by*imageHeight/bufHeight/2, (2*by+1)*imageHeight/bufHeight/2) + ll := self.colorAverage(2*bx*imageWidth/bufWidth/2, (2*bx+1)*imageWidth/bufWidth/2, (2*by+1)*imageHeight/bufHeight/2, (2*by+2)*imageHeight/bufHeight/2) + lr := self.colorAverage((2*bx+1)*imageWidth/bufWidth/2, (2*bx+2)*imageWidth/bufWidth/2, (2*by+1)*imageHeight/bufHeight/2, (2*by+2)*imageHeight/bufHeight/2) + buf.SetCell( + NewCell(blocksChar(ul, ur, ll, lr, self.MonochromeThreshold, self.MonochromeInvert)), + image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by), + ) + } + } + } else { + if bufWidth > imageWidth { + bufWidth = imageWidth + } + if bufHeight > imageHeight { + bufHeight = imageHeight + } + for bx := 0; bx < bufWidth; bx++ { + for by := 0; by < bufHeight; by++ { + c := self.colorAverage( + bx*imageWidth/bufWidth, + (bx+1)*imageWidth/bufWidth, + by*imageHeight/bufHeight, + (by+1)*imageHeight/bufHeight, + ) + buf.SetCell( + Cell{ + Rune: c.ch(), + Style: NewStyle( + c.fgColor(), + ColorBlack, + ), + }, + image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by), + ) + } + } + } +} + +func (self *Image) colorAverage(x0, x1, y0, y1 int) colorAverager { + var c colorAverager + for x := x0; x < x1; x++ { + for y := y0; y < y1; y++ { + c = c.add(self.Image.At(x+self.Image.Bounds().Min.X, y+self.Image.Bounds().Min.Y)) + } + } + return c +} + +type colorAverager struct { + rsum, gsum, bsum, asum, count uint64 +} + +func (self colorAverager) add(col color.Color) colorAverager { + r, g, b, a := col.RGBA() + return colorAverager{ + rsum: self.rsum + uint64(r), + gsum: self.gsum + uint64(g), + bsum: self.bsum + uint64(b), + asum: self.asum + uint64(a), + count: self.count + 1, + } +} + +func (self colorAverager) RGBA() (uint32, uint32, uint32, uint32) { + if self.count == 0 { + return 0, 0, 0, 0 + } else { + return uint32(self.rsum/self.count) & 0xffff, + uint32(self.gsum/self.count) & 0xffff, + uint32(self.bsum/self.count) & 0xffff, + uint32(self.asum/self.count) & 0xffff + } +} + +func (self colorAverager) fgColor() Color { + return palette.Convert(self).(paletteColor).attribute +} + +func (self colorAverager) ch() rune { + gray := color.GrayModel.Convert(self).(color.Gray).Y + switch { + case gray < 51: + return SHADED_BLOCKS[0] + case gray < 102: + return SHADED_BLOCKS[1] + case gray < 153: + return SHADED_BLOCKS[2] + case gray < 204: + return SHADED_BLOCKS[3] + default: + return SHADED_BLOCKS[4] + } +} + +func (self colorAverager) monochrome(threshold uint8, invert bool) bool { + return self.count != 0 && (color.GrayModel.Convert(self).(color.Gray).Y < threshold != invert) +} + +type paletteColor struct { + rgba color.RGBA + attribute Color +} + +func (self paletteColor) RGBA() (uint32, uint32, uint32, uint32) { + return self.rgba.RGBA() +} + +var palette = color.Palette([]color.Color{ + paletteColor{color.RGBA{0, 0, 0, 255}, ColorBlack}, + paletteColor{color.RGBA{255, 0, 0, 255}, ColorRed}, + paletteColor{color.RGBA{0, 255, 0, 255}, ColorGreen}, + paletteColor{color.RGBA{255, 255, 0, 255}, ColorYellow}, + paletteColor{color.RGBA{0, 0, 255, 255}, ColorBlue}, + paletteColor{color.RGBA{255, 0, 255, 255}, ColorMagenta}, + paletteColor{color.RGBA{0, 255, 255, 255}, ColorCyan}, + paletteColor{color.RGBA{255, 255, 255, 255}, ColorWhite}, +}) + +func blocksChar(ul, ur, ll, lr colorAverager, threshold uint8, invert bool) rune { + index := 0 + if ul.monochrome(threshold, invert) { + index |= 1 + } + if ur.monochrome(threshold, invert) { + index |= 2 + } + if ll.monochrome(threshold, invert) { + index |= 4 + } + if lr.monochrome(threshold, invert) { + index |= 8 + } + return IRREGULAR_BLOCKS[index] +} diff --git a/widgets/piechart.go b/widgets/piechart.go index 7d732ab..5313a39 100644 --- a/widgets/piechart.go +++ b/widgets/piechart.go @@ -56,7 +56,7 @@ func (self *PieChart) Draw(buf *Buffer) { for j := 0.0; j < size; j += resolutionFactor { borderPoint := borderCircle.at(phi + j) line := line{P1: center, P2: borderPoint} - line.draw(NewCell(SHADED_BLOCK, NewStyle(SelectColor(self.Colors, i))), buf) + line.draw(NewCell(SHADED_BLOCKS[1], NewStyle(SelectColor(self.Colors, i))), buf) } phi += size } diff --git a/widgets/sparkline.go b/widgets/sparkline.go index ea6a856..e70a8df 100644 --- a/widgets/sparkline.go +++ b/widgets/sparkline.go @@ -74,7 +74,7 @@ func (self *SparklineGroup) Draw(buf *Buffer) { ) } if height == 0 { - sparkChar = BARS[0] + sparkChar = BARS[1] buf.SetCell( NewCell(sparkChar, NewStyle(sl.LineColor)), image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset),