First run at basic structure
This commit is contained in:
209
internal/ui/hud/elements.go
Normal file
209
internal/ui/hud/elements.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package hud
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/status"
|
||||
)
|
||||
|
||||
var (
|
||||
basicFace = text.NewGoXFace(basicfont.Face7x13)
|
||||
basicFaceAscent = basicFace.Metrics().HAscent
|
||||
)
|
||||
|
||||
func drawHUDText(screen *ebiten.Image, txt string, clr color.Color, x, y int) {
|
||||
if clr == nil {
|
||||
clr = color.White
|
||||
}
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(float64(x), float64(y)-basicFaceAscent)
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
text.Draw(screen, txt, basicFace, op)
|
||||
}
|
||||
|
||||
// Drawable HUD chunk.
|
||||
type Element interface {
|
||||
Draw(screen *ebiten.Image, x, y int) (width, height int)
|
||||
}
|
||||
|
||||
// Plain text node.
|
||||
type Label struct {
|
||||
Text string
|
||||
Color color.Color
|
||||
}
|
||||
|
||||
func (l Label) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
if l.Color == nil {
|
||||
l.Color = color.White
|
||||
}
|
||||
drawHUDText(screen, l.Text, l.Color, x, y)
|
||||
width := len(l.Text) * 7 // approximate width for basicfont
|
||||
return width, 13
|
||||
}
|
||||
|
||||
// Percent readout node.
|
||||
type PercentageDisplay struct {
|
||||
Meter status.Meter
|
||||
Color color.Color
|
||||
}
|
||||
|
||||
func (p PercentageDisplay) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
if p.Color == nil {
|
||||
p.Color = color.White
|
||||
}
|
||||
txt := fmt.Sprintf("%3.0f%%", p.Meter.Level)
|
||||
drawHUDText(screen, txt, p.Color, x, y)
|
||||
return len(txt) * 7, 13
|
||||
}
|
||||
|
||||
// Combined label and percent.
|
||||
type MeterLabel struct {
|
||||
Meter status.Meter
|
||||
Color color.Color
|
||||
}
|
||||
|
||||
func (m MeterLabel) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
if m.Color == nil {
|
||||
m.Color = color.White
|
||||
}
|
||||
txt := fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level)
|
||||
drawHUDText(screen, txt, m.Color, x, y)
|
||||
return len(txt) * 7, 13
|
||||
}
|
||||
|
||||
// Horizontal meter bar.
|
||||
type Bar struct {
|
||||
Meter status.Meter
|
||||
MaxWidth int
|
||||
Height int
|
||||
BorderColor color.Color
|
||||
ShowBorder bool
|
||||
}
|
||||
|
||||
func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
maxWidth := b.MaxWidth
|
||||
if maxWidth <= 0 {
|
||||
maxWidth = 180
|
||||
}
|
||||
height := b.Height
|
||||
if height <= 0 {
|
||||
height = 8
|
||||
}
|
||||
|
||||
fillWidth := int((b.Meter.Level / 100.0) * float64(maxWidth))
|
||||
if fillWidth < 0 {
|
||||
fillWidth = 0
|
||||
}
|
||||
if fillWidth > maxWidth {
|
||||
fillWidth = maxWidth
|
||||
}
|
||||
|
||||
// Draw border if requested
|
||||
if b.ShowBorder {
|
||||
borderColor := b.BorderColor
|
||||
if borderColor == nil {
|
||||
borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255}
|
||||
}
|
||||
// Top border
|
||||
drawRect(screen, x, y, maxWidth, 1, borderColor)
|
||||
// Bottom border
|
||||
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
|
||||
// Left border
|
||||
drawRect(screen, x, y, 1, height, borderColor)
|
||||
// Right border
|
||||
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
|
||||
}
|
||||
|
||||
// Draw filled portion
|
||||
if fillWidth > 0 {
|
||||
drawRect(screen, x, y, fillWidth, height, b.Meter.Color)
|
||||
}
|
||||
|
||||
return maxWidth, height
|
||||
}
|
||||
|
||||
// Helper for filled rectangles.
|
||||
func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
|
||||
if width <= 0 || height <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a small 1x1 pixel image and scale it
|
||||
img := ebiten.NewImage(1, 1)
|
||||
img.Fill(clr)
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(float64(width), float64(height))
|
||||
op.GeoM.Translate(float64(x), float64(y))
|
||||
|
||||
screen.DrawImage(img, op)
|
||||
}
|
||||
|
||||
// Empty padding block.
|
||||
type Spacer struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
func (s Spacer) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
return s.Width, s.Height
|
||||
}
|
||||
|
||||
// Horizontal layout row.
|
||||
type Row struct {
|
||||
Elements []Element
|
||||
Spacing int
|
||||
}
|
||||
|
||||
func (r Row) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
totalWidth := 0
|
||||
maxHeight := 0
|
||||
currentX := x
|
||||
|
||||
for i, elem := range r.Elements {
|
||||
w, h := elem.Draw(screen, currentX, y)
|
||||
totalWidth += w
|
||||
if h > maxHeight {
|
||||
maxHeight = h
|
||||
}
|
||||
currentX += w
|
||||
if i < len(r.Elements)-1 {
|
||||
currentX += r.Spacing
|
||||
totalWidth += r.Spacing
|
||||
}
|
||||
}
|
||||
|
||||
return totalWidth, maxHeight
|
||||
}
|
||||
|
||||
// Vertical stack layout.
|
||||
type Column struct {
|
||||
Elements []Element
|
||||
Spacing int
|
||||
}
|
||||
|
||||
func (c Column) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||
maxWidth := 0
|
||||
totalHeight := 0
|
||||
currentY := y
|
||||
|
||||
for i, elem := range c.Elements {
|
||||
w, h := elem.Draw(screen, x, currentY)
|
||||
if w > maxWidth {
|
||||
maxWidth = w
|
||||
}
|
||||
totalHeight += h
|
||||
currentY += h
|
||||
if i < len(c.Elements)-1 {
|
||||
currentY += c.Spacing
|
||||
totalHeight += c.Spacing
|
||||
}
|
||||
}
|
||||
|
||||
return maxWidth, totalHeight
|
||||
}
|
||||
52
internal/ui/hud/hud.go
Normal file
52
internal/ui/hud/hud.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package hud
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/status"
|
||||
)
|
||||
|
||||
// HUD overlay anchor.
|
||||
type Overlay struct {
|
||||
X int
|
||||
Y int
|
||||
Color color.Color
|
||||
}
|
||||
|
||||
// Paints the HUD overlay.
|
||||
func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
||||
if o.Color == nil {
|
||||
o.Color = color.White
|
||||
}
|
||||
|
||||
// Instruction text
|
||||
instructions := Column{
|
||||
Elements: []Element{
|
||||
Label{Text: "Systems Prototype", Color: o.Color},
|
||||
Label{Text: "Move with Arrow Keys / WASD", Color: o.Color},
|
||||
Label{Text: "Track resource signals and plan ahead.", Color: o.Color},
|
||||
},
|
||||
Spacing: 7,
|
||||
}
|
||||
instructions.Draw(screen, 16, 16)
|
||||
|
||||
// Meter column
|
||||
meterElements := make([]Element, 0, len(meters))
|
||||
for _, meter := range meters {
|
||||
meterElements = append(meterElements, Column{
|
||||
Elements: []Element{
|
||||
MeterLabel{Meter: meter, Color: o.Color},
|
||||
Bar{Meter: meter, MaxWidth: 180, Height: 8, ShowBorder: false},
|
||||
},
|
||||
Spacing: 2,
|
||||
})
|
||||
}
|
||||
|
||||
meterPanel := Column{
|
||||
Elements: meterElements,
|
||||
Spacing: 16,
|
||||
}
|
||||
meterPanel.Draw(screen, o.X, o.Y)
|
||||
}
|
||||
Reference in New Issue
Block a user