hwinfo-streamdeck/pkg/graph/graph.go

328 lines
7.2 KiB
Go

package graph
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"math"
"regexp"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"image"
"image/color"
"image/png"
"sync"
)
// Label struct contains text, position and color information
type Label struct {
text string
y uint
fontSize float64
clr *color.RGBA
}
// Graph is used to display a histogram of data passed to Update
type Graph struct {
img *image.RGBA
lvay int
width int
height int
min int
max int
yvals []uint8
fgColor *color.RGBA
bgColor *color.RGBA
hlColor *color.RGBA
labels map[int]*Label
drawn bool
redraw bool
}
// FontFaceManager builds and caches fonts based on size
type FontFaceManager struct {
mux sync.Mutex
fontCache map[float64]font.Face
}
// NewFontFaceManager constructs new manager
func NewFontFaceManager() *FontFaceManager {
return &FontFaceManager{fontCache: make(map[float64]font.Face)}
}
func (f *FontFaceManager) newFace(size float64) font.Face {
b, err := ioutil.ReadFile("DejaVuSans-Bold.ttf")
if err != nil {
log.Fatal(err)
}
tt, err := truetype.Parse(b)
if err != nil {
log.Fatal("failed to parse font")
}
face := truetype.NewFace(tt, &truetype.Options{Size: size, DPI: 72})
return face
}
// GetFaceOfSize returns font face for given size
func (f *FontFaceManager) GetFaceOfSize(size float64) font.Face {
f.mux.Lock()
defer f.mux.Unlock()
if f, ok := f.fontCache[size]; ok {
return f
}
nf := f.newFace(size)
f.fontCache[size] = nf
return nf
}
type singleshared struct {
fontFaceManager *FontFaceManager
pngEnc *png.Encoder
pngBuf *bytes.Buffer
}
var sharedinstance *singleshared
var once sync.Once
func shared() *singleshared {
once.Do(func() {
sharedinstance = &singleshared{
pngEnc: &png.Encoder{
CompressionLevel: png.NoCompression,
},
pngBuf: bytes.NewBuffer(make([]byte, 0, 15697)),
}
sharedinstance.fontFaceManager = NewFontFaceManager()
})
return sharedinstance
}
// NewGraph initializes a new Graph for rendering
func NewGraph(width, height, min, max int, fgColor, bgColor, hlColor *color.RGBA) *Graph {
img := image.NewRGBA(image.Rect(0, 0, int(width), int(height)))
labels := make(map[int]*Label)
return &Graph{
img: img,
lvay: -1,
width: width,
height: height,
min: min,
max: max,
labels: labels,
yvals: make([]uint8, 0, width),
fgColor: fgColor,
bgColor: bgColor,
hlColor: hlColor,
}
}
// SetForegroundColor sets the foreground color of the graph
func (g *Graph) SetForegroundColor(clr *color.RGBA) {
g.fgColor = clr
g.redraw = true
}
// SetBackgroundColor sets the background color of the graph
func (g *Graph) SetBackgroundColor(clr *color.RGBA) {
g.bgColor = clr
g.redraw = true
}
// SetHighlightColor sets the highlight color of the graph
func (g *Graph) SetHighlightColor(clr *color.RGBA) {
g.hlColor = clr
g.redraw = true
}
// SetMin sets the min value for the graph scale
func (g *Graph) SetMin(min int) {
g.min = min
}
// SetMax sets the max value for the graph scale
func (g *Graph) SetMax(max int) {
g.max = max
}
// SetLabel given a key, set the initial text, position and color
func (g *Graph) SetLabel(key int, text string, y uint, clr *color.RGBA) {
l := &Label{text: text, y: y, clr: clr}
g.labels[key] = l
}
// SetLabelText given a key, update the text for a pre-set label
func (g *Graph) SetLabelText(key int, text string) error {
l, ok := g.labels[key]
if !ok {
return fmt.Errorf("Label with key (%d) does not exist", key)
}
l.text = text
return nil
}
// SetLabelFontSize given a key, update the text for a pre-set label
func (g *Graph) SetLabelFontSize(key int, size float64) error {
l, ok := g.labels[key]
if !ok {
return fmt.Errorf("Label with key (%d) does not exist", key)
}
l.fontSize = size
return nil
}
// SetLabelColor given a key and color, sets the color of the text
func (g *Graph) SetLabelColor(key int, clr *color.RGBA) error {
l, ok := g.labels[key]
if !ok {
return fmt.Errorf("Label with key (%d) does not exist", key)
}
l.clr = clr
return nil
}
func (g *Graph) drawGraph(x, vay, maxx int) {
var clr *color.RGBA
for ; x <= maxx; x++ {
for y := 0; y < g.height; y++ {
if y == vay {
clr = g.hlColor
} else if g.lvay != -1 && vay > g.lvay && vay >= y && y >= g.lvay {
clr = g.hlColor
} else if g.lvay != -1 && vay < g.lvay && vay <= y && y <= g.lvay {
clr = g.hlColor
} else if vay > y {
clr = g.fgColor
} else {
clr = g.bgColor
}
i := g.img.PixOffset(x, g.width-1-y)
g.img.Pix[i+0] = clr.R
g.img.Pix[i+1] = clr.G
g.img.Pix[i+2] = clr.B
g.img.Pix[i+3] = clr.A
}
g.lvay = vay
}
}
// Update given a value draws the graph, shifting contents left. Call EncodePNG to get a rendered PNG
func (g *Graph) Update(value float64) {
vay := vAsY(g.height-1, value, g.min, g.max)
if len(g.yvals) >= g.width {
_, a := g.yvals[0], g.yvals[1:]
g.yvals = a
}
g.yvals = append(g.yvals, uint8(vay))
if g.redraw {
g.lvay = -1
lyvals := len(g.yvals)
for idx := lyvals - 1; idx >= 0; idx-- {
x := g.width - lyvals + idx
maxx := x
if idx == 0 {
x = 0
}
v := int(g.yvals[idx])
g.drawGraph(x, v, maxx)
}
g.lvay = int(g.yvals[lyvals-1])
g.redraw = false
} else if g.drawn {
// shift the graph left 1px
for y := 0; y < g.height; y++ {
idx := g.img.PixOffset(0, y)
p1 := g.img.Pix[:idx]
p2 := g.img.Pix[idx+4 : idx+(g.width*4)]
p3 := g.img.Pix[idx+(g.width*4):]
g.img.Pix = append(p1, append(append(p2, []uint8{0, 0, 0, 0}...), p3...)...)
}
g.drawGraph(int(g.width)-1, int(vay), g.width-1)
} else {
g.drawGraph(0, vay, g.width-1)
g.drawn = true
}
}
// EncodePNG renders the current state of the graph
func (g *Graph) EncodePNG() ([]byte, error) {
bak := append(g.img.Pix[:0:0], g.img.Pix...)
for _, l := range g.labels {
g.drawLabel(l)
}
shared := shared()
err := shared.pngEnc.Encode(shared.pngBuf, g.img)
if err != nil {
return nil, err
}
g.img.Pix = bak
bts := shared.pngBuf.Bytes()
shared.pngBuf.Reset()
return bts, nil
}
func vAsY(maxY int, v float64, minV, maxV int) int {
r := maxV - minV
v1 := v - float64(minV)
yf := v1 / float64(r) * float64(maxY)
yi := int(math.Round(yf))
return yi
}
func unfix(x fixed.Int26_6) float64 {
const shift, mask = 6, 1<<6 - 1
if x >= 0 {
return float64(x>>shift) + float64(x&mask)/64
}
x = -x
if x >= 0 {
return -(float64(x>>shift) + float64(x&mask)/64)
}
return 0
}
var newlineRegex = regexp.MustCompile("(\n|\\\\n)+")
func (g *Graph) drawLabel(l *Label) {
shared := shared()
lines := newlineRegex.Split(l.text, -1)
face := shared.fontFaceManager.GetFaceOfSize(l.fontSize)
curY := l.y - uint(10.5-float64(face.Metrics().Height.Round()))
for _, line := range lines {
var lwidth float64
for _, x := range line {
awidth, ok := face.GlyphAdvance(rune(x))
if ok != true {
log.Println("drawLabel: Failed to GlyphAdvance")
return
}
lwidth += unfix(awidth)
}
lx := (float64(g.width) / 2.) - (lwidth / 2.)
point := fixed.Point26_6{X: fixed.Int26_6(lx * 64), Y: fixed.Int26_6(curY * 64)}
d := &font.Drawer{
Dst: g.img,
Src: image.NewUniform(l.clr),
Face: face,
Dot: point,
}
d.DrawString(line)
curY += 12
}
}