From a7b3a6cd4af33b9a8c6c43c5487254eb844bd3c1 Mon Sep 17 00:00:00 2001 From: Ben Vezzani Date: Mon, 18 Aug 2025 14:57:01 -0400 Subject: [PATCH 1/4] ux stuff --- common/control/v1/mouse.go | 8 ++ common/elements/v1/block.go | 61 +++++++++++++++ common/elements/v1/button.go | 76 ++++++++++++++++++ common/elements/v1/element.go | 92 ++++++++++++++++++++++ common/elements/v1/menu.go | 1 + common/elements/v1/table.go | 100 ++++++++++++++++++++++++ common/ux/v1/ux.go | 13 +++ games/minesweeper/{ => v1}/game/game.go | 0 games/minesweeper/{ => v1}/main.go | 4 +- 9 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 common/control/v1/mouse.go create mode 100644 common/elements/v1/block.go create mode 100644 common/elements/v1/button.go create mode 100644 common/elements/v1/element.go create mode 100644 common/elements/v1/menu.go create mode 100644 common/elements/v1/table.go create mode 100644 common/ux/v1/ux.go rename games/minesweeper/{ => v1}/game/game.go (100%) rename games/minesweeper/{ => v1}/main.go (74%) diff --git a/common/control/v1/mouse.go b/common/control/v1/mouse.go new file mode 100644 index 0000000..73e5121 --- /dev/null +++ b/common/control/v1/mouse.go @@ -0,0 +1,8 @@ +package control + +const ( + MouseLeft MouseButton = iota + MouseRight +) + +type MouseButton int diff --git a/common/elements/v1/block.go b/common/elements/v1/block.go new file mode 100644 index 0000000..4c71109 --- /dev/null +++ b/common/elements/v1/block.go @@ -0,0 +1,61 @@ +package elements + +import ( + "context" + "image/color" + + "git.vezzani.net/ben/games/common/ux/v1" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" + "golang.org/x/image/font" +) + +type Block struct { + Label string + Style struct { + Width float32 + Height float32 + XAlign XAlign + YAlign YAlign + Offset int + Font *font.Face + BackgroundColor *color.Color + } +} + +func (b *Block) backgroundColor() color.Color { + var c *color.Color + if b.Style.BackgroundColor != nil { + c = b.Style.BackgroundColor + } else { + c = &ux.BackgroundColor + } + + return *c +} + +func (b *Block) size(ctx context.Context) (x, y float32) { + x = b.Style.Width + y = b.Style.Height + + if x == 0 || y == 0 { + px, py := GetContainerSize(ctx) + if x == 0 { + x = px + } + if y == 0 { + y = py + } + } + + return +} + +func (b *Block) Draw(ctx context.Context, image *ebiten.Image) error { + xz, yz := GetZero(ctx) + w, h := b.size(ctx) + + vector.StrokeRect(image, xz, yz, w, h, 1, b.backgroundColor(), true) + + return nil +} diff --git a/common/elements/v1/button.go b/common/elements/v1/button.go new file mode 100644 index 0000000..8d21ce3 --- /dev/null +++ b/common/elements/v1/button.go @@ -0,0 +1,76 @@ +package elements + +import ( + "context" + "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" + "golang.org/x/image/font" +) +import "git.vezzani.net/ben/games/common/ux/v1" + +type XAlign int +type YAlign int + +const ( + AlignCente XAlign = iota + AlignLeft + AlignRight +) + +const ( + AlignCenter YAlign = iota + AlignTop + AlignBottom +) + +type Button struct { + mouseHandler + Block + Label string + OnClick func() error + OnRightClick func() error + Style struct { + MouseDownColor *color.Color + } +} + +func (b *Button) HandleClick(_ MouseState) error { + if b.OnClick == nil { + return nil + } + return b.OnClick() +} + +func (b *Button) getFont() font.Face { + if b.Block.Style.Font == nil { + return ux.FontFace + } + + return *b.Block.Style.Font +} + +func (b *Button) backgroundColor() color.Color { + var c *color.Color + if b.Block.Style.BackgroundColor != nil { + c = b.Block.Style.BackgroundColor + } else { + c = &ux.BackgroundColor + } + + if (b.mouseState.RightDown || b.mouseState.LeftDown) && b.Style.MouseDownColor != nil { + c = b.Style.MouseDownColor + } + + return *c +} + +func (b *Button) Draw(ctx context.Context, image *ebiten.Image) error { + xz, yz := GetZero(ctx) + w, h := b.size(ctx) + + vector.StrokeRect(image, xz, yz, w, h, 1, b.backgroundColor(), true) + + return nil +} diff --git a/common/elements/v1/element.go b/common/elements/v1/element.go new file mode 100644 index 0000000..411c511 --- /dev/null +++ b/common/elements/v1/element.go @@ -0,0 +1,92 @@ +package elements + +import ( + "context" + + "github.com/hajimehoshi/ebiten/v2" +) + +type MouseState struct { + X, Y int32 + LeftDown, RightDown bool + LeftChanged, RightChanged bool +} + +const ( + ContainerSizeKey = "container_size" + ZeroKey = "zero" +) + +type Clickable interface { + Element + HandleClick(ctx context.Context, s MouseState) error +} + +type Mouseable interface { + Element + HandleMouseEnter(ctx context.Context, s MouseState) error + HandleMouseLeave(ctx context.Context, s MouseState) error + HandleMouseMove(ctx context.Context, s MouseState) error + HandleMouseDown(ctx context.Context, s MouseState) error + HandleMouseUp(ctx context.Context, s MouseState) error +} + +type Element interface { + Draw(ctx context.Context, image *ebiten.Image) error +} + +func SetZero(ctx context.Context, x, y float32) context.Context { + return context.WithValue(ctx, ZeroKey, [2]float32{x, y}) +} + +func SetContainerSize(ctx context.Context, w, h float32) context.Context { + return context.WithValue(ctx, ContainerSizeKey, [2]float32{w, h}) +} + +func GetZero(ctx context.Context) (x, y float32) { + if v, ok := ctx.Value(ZeroKey).([2]float32); ok { + return v[0], v[1] + } + return 0, 0 +} + +func GetContainerSize(ctx context.Context) (w, h float32) { + if s, ok := ctx.Value(ContainerSizeKey).([2]float32); ok { + return s[0], s[1] + } + return 0, 0 +} + +type mouseHandler struct { + mouseState MouseState + prevMouseState MouseState +} + +func (b *mouseHandler) HandleMouseEnter(s MouseState) error { + b.mouseState = s + return nil +} + +func (b *mouseHandler) HandleMouseLeave(s MouseState) error { + b.prevMouseState = b.mouseState + b.mouseState = MouseState{} + return nil +} + +func (b *mouseHandler) HandleMouseMove(s MouseState) error { + b.prevMouseState = b.mouseState + b.mouseState = s + return nil +} + +func (b *mouseHandler) HandleMouseDown(s MouseState) error { + b.prevMouseState = b.mouseState + b.mouseState = s + return nil +} + +func (b *mouseHandler) HandleMouseUp(s MouseState) error { + b.prevMouseState = b.mouseState + b.mouseState = s + return nil +} diff --git a/common/elements/v1/menu.go b/common/elements/v1/menu.go new file mode 100644 index 0000000..8a64ab6 --- /dev/null +++ b/common/elements/v1/menu.go @@ -0,0 +1 @@ +package elements diff --git a/common/elements/v1/table.go b/common/elements/v1/table.go new file mode 100644 index 0000000..34af0f5 --- /dev/null +++ b/common/elements/v1/table.go @@ -0,0 +1,100 @@ +package elements + +import ( + "context" + "fmt" + + "github.com/hajimehoshi/ebiten/v2" +) + +type Table struct { + mouseHandler + Block + + ColumnCount int + RowCount int + + Cells []Element + + Style struct { + } + + mouseOverCell int +} + +func (t *Table) HandleClick(ctx context.Context, s MouseState) error { + address := t.getAddressUnderMouse() + if address < 0 || address >= len(t.Cells) { + return fmt.Errorf("cell address under mouse is out of bounds") + } + + if clickable, ok := t.Cells[address].(Clickable); ok { + return clickable.HandleClick(s) + } + + return nil +} + +func (t *Table) HandleMouseEnter(ctx context.Context, s MouseState) error { + _ = t.mouseHandler.HandleMouseEnter(s) + address := t.getAddressUnderMouse() + if address < 0 || address >= len(t.Cells) { + return fmt.Errorf("cell address under mouse is out of bounds") + } + + if mouseable, ok := t.Cells[address].(Mouseable); ok { + s.X -= + return mouseable.HandleMouseEnter(s) + } + + return nil +} + +func (t *Table) HandleMouseLeave(ctx context.Context, s MouseState) error { + _ = t.mouseHandler.HandleMouseLeave(s) + return nil +} + +func (t *Table) HandleMouseMove(ctx context.Context, s MouseState) error { + _ = t.mouseHandler.HandleMouseMove(s) + return nil +} + +func (t *Table) HandleMouseDown(ctx context.Context, s MouseState) error { + _ = t.mouseHandler.HandleMouseDown(s) + return nil +} + +func (t *Table) HandleMouseUp(ctx context.Context, s MouseState) error { + _ = t.mouseHandler.HandleMouseUp(s) + return nil +} + +func (t *Table) CellSize(ctx context.Context) (w, h float32) { + w, h = GetContainerSize(ctx) + return w / float32(t.ColumnCount), h / float32(t.RowCount) +} + +func (t *Table) CellZero(ctx context.Context, addr int) (x, y float32) { + x, y = GetZero(ctx) + w, h := t.CellSize(ctx) + + col, row := addr%t.ColumnCount, addr/t.ColumnCount + return x + float32(col)*w, y + float32(row) + h +} + +func (t *Table) Draw(ctx context.Context, screen *ebiten.Image) error { + +} + +func (t *Table) contextForCell(ctx context.Context, addr int) context.Context { + zx, zy := t.CellZero(ctx, addr) + w, h := t.CellSize(ctx) + ctx = SetZero(ctx, zx, zy) + ctx = SetContainerSize(ctx, w, h) + return ctx +} + +func (t *Table) getAddressUnderMouse() int { + return int(t.mouseState.Y)*t.RowCount + int(t.mouseState.X) +} diff --git a/common/ux/v1/ux.go b/common/ux/v1/ux.go new file mode 100644 index 0000000..03490e5 --- /dev/null +++ b/common/ux/v1/ux.go @@ -0,0 +1,13 @@ +package ux + +import ( + "image/color" + + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" +) + +var ( + FontFace font.Face = basicfont.Face7x13 + BackgroundColor color.Color = color.White +) diff --git a/games/minesweeper/game/game.go b/games/minesweeper/v1/game/game.go similarity index 100% rename from games/minesweeper/game/game.go rename to games/minesweeper/v1/game/game.go diff --git a/games/minesweeper/main.go b/games/minesweeper/v1/main.go similarity index 74% rename from games/minesweeper/main.go rename to games/minesweeper/v1/main.go index b23f639..8a6b190 100644 --- a/games/minesweeper/main.go +++ b/games/minesweeper/v1/main.go @@ -3,12 +3,12 @@ package main import ( "log" - "git.vezzani.net/ben/games/games/minesweeper/game" + "git.vezzani.net/ben/games/games/minesweeper/v1/game" "github.com/hajimehoshi/ebiten/v2" ) func main() { - g := game.New(10, 10, 50) + g := game.New(10, 10, 15) w, h := g.Layout(0, 0) ebiten.SetWindowSize(w, h) ebiten.SetWindowTitle("Minesweeper") From 8b1bfc8a01998a9a735576d9bc5632673e6c543b Mon Sep 17 00:00:00 2001 From: Ben Vezzani Date: Mon, 25 Aug 2025 11:56:44 -0400 Subject: [PATCH 2/4] sprite stuff --- common/sprites/v1/sprite.go | 133 ++++++++++++++++++++++++++++++++++++ common/ux/v1/ux.go | 1 + go.mod | 17 +++-- go.sum | 46 +++++++++---- tools/spritedit/main.go | 90 ++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 common/sprites/v1/sprite.go create mode 100644 tools/spritedit/main.go diff --git a/common/sprites/v1/sprite.go b/common/sprites/v1/sprite.go new file mode 100644 index 0000000..fda3b21 --- /dev/null +++ b/common/sprites/v1/sprite.go @@ -0,0 +1,133 @@ +package sprites + +import ( + "image" + + "git.vezzani.net/ben/games/common/window/v1" + "github.com/hajimehoshi/ebiten/v2" +) + +var ( + updateCount int +) + +func Update() { + updateCount++ +} + +type animation struct { + RowNumber uint8 + FrameCount uint8 + Scale float32 +} + +type subImager interface { + image.Image + SubImage(r image.Rectangle) image.Image +} + +type Sprite struct { + width, height int + imgData subImager + animations map[string]animation + + baseAnim *animation +} + +func (s *Sprite) baseAnimation() animation { + if s.baseAnim != nil { + return *s.baseAnim + } + + for i := range s.animations { + if s.animations[i].RowNumber == 0 { + a := s.animations[i] + s.baseAnim = &a + return a + } + } + + return animation{} +} + +func (s *Sprite) getAnimation(id string) animation { + if id == "" { + return s.baseAnimation() + } + + if anim, ok := s.animations[id]; ok { + return anim + } + + return s.baseAnimation() +} + +func (s *Sprite) Image(ops imageOptions) image.Image { + anim := s.getAnimation(ops.animation) + + xOffset := updateCount % int(anim.FrameCount) * s.width + yOffset := int(anim.RowNumber) * s.height + + r := image.Rect(xOffset, yOffset, s.width, s.height).Intersect(s.imgData.Bounds()) + + return s.imgData.SubImage(r) +} + +func (s *Sprite) Draw(screen *ebiten.Image, options ...ImageOption) { + ops := imageOptions{ + scaleX: window.Scale, + scaleY: window.Scale, + } + + for _, o := range options { + o(&ops) + } + + geom := ebiten.GeoM{} + geom.Translate(ops.x, ops.y) + geom.Scale(ops.scaleX, ops.scaleY) + geom.Rotate(ops.rotateTheta) + + screen.DrawImage( + ebiten.NewImageFromImage( + s.Image(ops), + ), + &ebiten.DrawImageOptions{ + GeoM: geom, + }, + ) +} + +type imageOptions struct { + x, y float64 + scaleX, scaleY float64 + rotateTheta float64 + animation string +} + +type ImageOption func(options *imageOptions) + +func ToScale(x, y float64) ImageOption { + return func(o *imageOptions) { + o.scaleX *= x + o.scaleY *= y + } +} + +func Animation(name string) ImageOption { + return func(o *imageOptions) { + o.animation = name + } +} + +func AtPosition(x, y float64) ImageOption { + return func(o *imageOptions) { + o.x, o.y = x, y + } +} + +func Rotate(theta float64) ImageOption { + return func(o *imageOptions) { + o.rotateTheta = theta + } +} diff --git a/common/ux/v1/ux.go b/common/ux/v1/ux.go index 03490e5..af6688a 100644 --- a/common/ux/v1/ux.go +++ b/common/ux/v1/ux.go @@ -10,4 +10,5 @@ import ( var ( FontFace font.Face = basicfont.Face7x13 BackgroundColor color.Color = color.White + Scale float64 = 1.0 ) diff --git a/go.mod b/go.mod index da0c2bb..298678f 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,22 @@ module git.vezzani.net/ben/games -go 1.24 +go 1.25 require ( + github.com/ebitenui/ebitenui v0.7.1 github.com/hajimehoshi/ebiten/v2 v2.8.8 - golang.org/x/image v0.20.0 + golang.org/x/image v0.25.0 ) require ( - github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect + github.com/ebitengine/gomobile v0.0.0-20250209143333-6071a2a2351c // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect - github.com/ebitengine/purego v0.8.0 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8 // indirect + github.com/go-text/typesetting v0.3.0 // indirect github.com/jezek/xgb v1.1.1 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 58e3580..5b25968 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,44 @@ -github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= -github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/gomobile v0.0.0-20250209143333-6071a2a2351c h1:nCxkoQoJMcVLc5aoMp3ULbfyEMcQjxopBKgNQVBQFXE= +github.com/ebitengine/gomobile v0.0.0-20250209143333-6071a2a2351c/go.mod h1:yMh1VvLL71zDgHlVlIXXJIGmv36QcJ9ZD2gtIGYAp3I= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= -github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= -github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitenui/ebitenui v0.7.1 h1:5ZOaonRs4EsO9LtVVJr1zXNBXadUA9ktaZQLg9QgWao= +github.com/ebitenui/ebitenui v0.7.1/go.mod h1:QiJoDflkWoBv4V/LKErS3cgzTZHrXDQyqajef7IA8vM= +github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8 h1:sdIsYe6Vv7KIWZWp8KqSeTl+XlF17d+wHCC4lbxFcYs= +github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8/go.mod h1:0QBxkXxN+o4FyZgLI9FHY/oUizheze3+bNY/kgCKL+4= +github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= +github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/hajimehoshi/bitmapfont/v3 v3.2.0 h1:0DISQM/rseKIJhdF29AkhvdzIULqNIIlXAGWit4ez1Q= github.com/hajimehoshi/bitmapfont/v3 v3.2.0/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg= github.com/hajimehoshi/ebiten/v2 v2.8.8 h1:xyMxOAn52T1tQ+j3vdieZ7auDBOXmvjUprSrxaIbsi8= github.com/hajimehoshi/ebiten/v2 v2.8.8/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= -golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -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= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/spritedit/main.go b/tools/spritedit/main.go new file mode 100644 index 0000000..4457d0b --- /dev/null +++ b/tools/spritedit/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "bytes" + + "github.com/ebitenui/ebitenui" + "github.com/ebitenui/ebitenui/image" + "github.com/ebitenui/ebitenui/widget" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text/v2" + "golang.org/x/image/colornames" + "golang.org/x/image/font/gofont/goregular" +) + +type Editor struct { + ui *ebitenui.UI +} + +func NewEditor() *Editor { + root := widget.NewContainer( + widget.ContainerOpts.BackgroundImage( + image.NewNineSliceColor(colornames.Gainsboro), + )) + root.AddChild( + widget.NewButton( + widget.ButtonOpts.TextLabel("Open Sprite Sheet..."), + widget.ButtonOpts.TextFace(DefaultFont()), + widget.ButtonOpts.TextColor(&widget.ButtonTextColor{ + Idle: colornames.Gainsboro, + Hover: colornames.Gainsboro, + Pressed: colornames.Gainsboro, + }), + widget.ButtonOpts.Image(&widget.ButtonImage{ + Idle: DefaultNineSlice(colornames.Darkslategray), + Hover: DefaultNineSlice(Mix(colornames.Darkslategray, colornames.Mediumseagreen, 0.4)), + Disabled: DefaultNineSlice(Mix(colornames.Darkslategray, colornames.Gainsboro, 0.8)), + Pressed: PressedNineSlice(Mix(colornames.Darkslategray, colornames.Black, 0.4)), + PressedHover: PressedNineSlice(Mix(colornames.Darkslategray, colornames.Black, 0.4)), + }), + widget.ButtonOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ + VerticalPosition: widget.AnchorLayoutPositionCenter, + HorizontalPosition: widget.AnchorLayoutPositionCenter, + }), + widget.WidgetOpts.MinSize(180, 48), + ), + ), + ) + + return &Editor{ + ui: &ebitenui.UI{Container: root}, + } +} + +func (e *Editor) Update() error { + e.ui.Update() + return nil +} + +func (e *Editor) Draw(screen *ebiten.Image) { + e.ui.Draw(screen) +} + +func (e *Editor) Layout(w, h int) (int, int) { + return w, h +} + +func DefaultFont() *text.Face { + s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF)) + if err != nil { + panic(err) + } + + var f text.Face + + f = &text.GoTextFace{ + Source: s, + Size: 20, + } + + return &f +} + +func main() { + ebiten.SetWindowSize(480, 320) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + if err := ebiten.RunGame(NewEditor()); err != nil { + panic(err) + } +} From 573396054f098560719f20838937f3b9951e3980 Mon Sep 17 00:00:00 2001 From: Ben Vezzani Date: Wed, 27 Aug 2025 23:22:39 -0400 Subject: [PATCH 3/4] okay new ux idea --- common/elements/v1/block.go | 65 ++++++++--------------------- common/elements/v1/button.go | 20 ++++----- common/elements/v1/element.go | 48 ++++++---------------- common/elements/v1/stack.go | 31 ++++++++++++++ common/elements/v1/table.go | 20 +++------ common/sprites/v1/sprite.go | 62 +++++++++++++++++----------- tools/spritedit/editor.go | 21 ++++++++++ tools/spritedit/main.go | 77 +---------------------------------- 8 files changed, 134 insertions(+), 210 deletions(-) create mode 100644 common/elements/v1/stack.go create mode 100644 tools/spritedit/editor.go diff --git a/common/elements/v1/block.go b/common/elements/v1/block.go index 4c71109..64d9a14 100644 --- a/common/elements/v1/block.go +++ b/common/elements/v1/block.go @@ -1,61 +1,28 @@ package elements import ( - "context" - "image/color" - - "git.vezzani.net/ben/games/common/ux/v1" "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/vector" - "golang.org/x/image/font" ) -type Block struct { - Label string - Style struct { - Width float32 - Height float32 - XAlign XAlign - YAlign YAlign - Offset int - Font *font.Face - BackgroundColor *color.Color - } -} +type BlockOption func(*block) -func (b *Block) backgroundColor() color.Color { - var c *color.Color - if b.Style.BackgroundColor != nil { - c = b.Style.BackgroundColor - } else { - c = &ux.BackgroundColor - } - - return *c -} - -func (b *Block) size(ctx context.Context) (x, y float32) { - x = b.Style.Width - y = b.Style.Height - - if x == 0 || y == 0 { - px, py := GetContainerSize(ctx) - if x == 0 { - x = px - } - if y == 0 { - y = py +func Block(ops ...BlockOption) ElementFunc { + return func(d Dimensions) Element { + b := block{} + for i := range ops { + ops[i](&b) } + return &b } - - return } -func (b *Block) Draw(ctx context.Context, image *ebiten.Image) error { - xz, yz := GetZero(ctx) - w, h := b.size(ctx) - - vector.StrokeRect(image, xz, yz, w, h, 1, b.backgroundColor(), true) - - return nil +type block struct { + width float64 + height float64 + xAlign xAlign + yAlign yAlign +} + +func (b block) Draw(image *ebiten.Image, anchorX, anchorY float64) (w, h float64) { + return b.width, b.height } diff --git a/common/elements/v1/button.go b/common/elements/v1/button.go index 8d21ce3..8a34c96 100644 --- a/common/elements/v1/button.go +++ b/common/elements/v1/button.go @@ -10,24 +10,24 @@ import ( ) import "git.vezzani.net/ben/games/common/ux/v1" -type XAlign int -type YAlign int +type xAlign int +type yAlign int const ( - AlignCente XAlign = iota + AlignCente xAlign = iota AlignLeft AlignRight ) const ( - AlignCenter YAlign = iota + AlignCenter yAlign = iota AlignTop AlignBottom ) type Button struct { mouseHandler - Block + block Label string OnClick func() error OnRightClick func() error @@ -44,17 +44,17 @@ func (b *Button) HandleClick(_ MouseState) error { } func (b *Button) getFont() font.Face { - if b.Block.Style.Font == nil { + if b.block.block.Font == nil { return ux.FontFace } - return *b.Block.Style.Font + return *b.block.block.Font } func (b *Button) backgroundColor() color.Color { var c *color.Color - if b.Block.Style.BackgroundColor != nil { - c = b.Block.Style.BackgroundColor + if b.block.block.BackgroundColor != nil { + c = b.block.block.BackgroundColor } else { c = &ux.BackgroundColor } @@ -67,8 +67,6 @@ func (b *Button) backgroundColor() color.Color { } func (b *Button) Draw(ctx context.Context, image *ebiten.Image) error { - xz, yz := GetZero(ctx) - w, h := b.size(ctx) vector.StrokeRect(image, xz, yz, w, h, 1, b.backgroundColor(), true) diff --git a/common/elements/v1/element.go b/common/elements/v1/element.go index 411c511..8e28b6f 100644 --- a/common/elements/v1/element.go +++ b/common/elements/v1/element.go @@ -1,60 +1,36 @@ package elements import ( - "context" - "github.com/hajimehoshi/ebiten/v2" ) +type ElementFunc func(Dimensions) Element + type MouseState struct { X, Y int32 LeftDown, RightDown bool LeftChanged, RightChanged bool } -const ( - ContainerSizeKey = "container_size" - ZeroKey = "zero" -) - type Clickable interface { - Element - HandleClick(ctx context.Context, s MouseState) error + HandleClick(s MouseState) error } type Mouseable interface { - Element - HandleMouseEnter(ctx context.Context, s MouseState) error - HandleMouseLeave(ctx context.Context, s MouseState) error - HandleMouseMove(ctx context.Context, s MouseState) error - HandleMouseDown(ctx context.Context, s MouseState) error - HandleMouseUp(ctx context.Context, s MouseState) error + HandleMouseEnter(s MouseState) error + HandleMouseLeave(s MouseState) error + HandleMouseMove(s MouseState) error + HandleMouseDown(s MouseState) error + HandleMouseUp(s MouseState) error } type Element interface { - Draw(ctx context.Context, image *ebiten.Image) error + Draw(image *ebiten.Image, anchorX, anchorY float64) (w, h float64) } -func SetZero(ctx context.Context, x, y float32) context.Context { - return context.WithValue(ctx, ZeroKey, [2]float32{x, y}) -} - -func SetContainerSize(ctx context.Context, w, h float32) context.Context { - return context.WithValue(ctx, ContainerSizeKey, [2]float32{w, h}) -} - -func GetZero(ctx context.Context) (x, y float32) { - if v, ok := ctx.Value(ZeroKey).([2]float32); ok { - return v[0], v[1] - } - return 0, 0 -} - -func GetContainerSize(ctx context.Context) (w, h float32) { - if s, ok := ctx.Value(ContainerSizeKey).([2]float32); ok { - return s[0], s[1] - } - return 0, 0 +type Dimensions struct { + zx, zy float64 + wx, wy float64 } type mouseHandler struct { diff --git a/common/elements/v1/stack.go b/common/elements/v1/stack.go new file mode 100644 index 0000000..c7450eb --- /dev/null +++ b/common/elements/v1/stack.go @@ -0,0 +1,31 @@ +package elements + +import "github.com/hajimehoshi/ebiten/v2" + +type StackOption func(*stack) + +func Stack(ops ...StackOption) ElementFunc { + return func(parentDimensions Dimensions) Element { + + } +} + +type stack struct { + block + horizontal bool + children []Element +} + +func (s *stack) Draw(image *ebiten.Image, anchorX, anchorY float64) (w, h float64) { + var offsetX, offsetY float64 + originalX, originalY := anchorX, anchorY + for i := range s.children { + offsetX, offsetY = s.children[i].Draw(image, anchorX, anchorY) + if s.horizontal { + anchorX += offsetX + } else { + anchorY += offsetY + } + } + return anchorX - originalX, anchorY - originalY +} diff --git a/common/elements/v1/table.go b/common/elements/v1/table.go index 34af0f5..e546916 100644 --- a/common/elements/v1/table.go +++ b/common/elements/v1/table.go @@ -9,12 +9,12 @@ import ( type Table struct { mouseHandler - Block + block ColumnCount int RowCount int - Cells []Element + Cells []Dimensions Style struct { } @@ -70,8 +70,8 @@ func (t *Table) HandleMouseUp(ctx context.Context, s MouseState) error { return nil } -func (t *Table) CellSize(ctx context.Context) (w, h float32) { - w, h = GetContainerSize(ctx) +func (t *Table) CellSize() (w, h float32) { + w, h = t. return w / float32(t.ColumnCount), h / float32(t.RowCount) } @@ -83,16 +83,8 @@ func (t *Table) CellZero(ctx context.Context, addr int) (x, y float32) { return x + float32(col)*w, y + float32(row) + h } -func (t *Table) Draw(ctx context.Context, screen *ebiten.Image) error { - -} - -func (t *Table) contextForCell(ctx context.Context, addr int) context.Context { - zx, zy := t.CellZero(ctx, addr) - w, h := t.CellSize(ctx) - ctx = SetZero(ctx, zx, zy) - ctx = SetContainerSize(ctx, w, h) - return ctx +func (t *Table) Draw(screen *ebiten.Image, zx, zy float64) error { + return nil } func (t *Table) getAddressUnderMouse() int { diff --git a/common/sprites/v1/sprite.go b/common/sprites/v1/sprite.go index fda3b21..9d32149 100644 --- a/common/sprites/v1/sprite.go +++ b/common/sprites/v1/sprite.go @@ -3,7 +3,7 @@ package sprites import ( "image" - "git.vezzani.net/ben/games/common/window/v1" + "git.vezzani.net/ben/games/common/ux/v1" "github.com/hajimehoshi/ebiten/v2" ) @@ -16,9 +16,9 @@ func Update() { } type animation struct { - RowNumber uint8 - FrameCount uint8 - Scale float32 + RowNumber uint8 + TickCount uint8 + Scale float32 } type subImager interface { @@ -31,7 +31,12 @@ type Sprite struct { imgData subImager animations map[string]animation - baseAnim *animation + baseAnim *animation + currentAnim *animation + + animationStartedAt int + animationStopAt int + animateOnce bool } func (s *Sprite) baseAnimation() animation { @@ -50,22 +55,17 @@ func (s *Sprite) baseAnimation() animation { return animation{} } -func (s *Sprite) getAnimation(id string) animation { - if id == "" { - return s.baseAnimation() +func (s *Sprite) getAnimation() animation { + if s.currentAnim != nil { + return *s.currentAnim } - - if anim, ok := s.animations[id]; ok { - return anim - } - return s.baseAnimation() } func (s *Sprite) Image(ops imageOptions) image.Image { - anim := s.getAnimation(ops.animation) + anim := s.getAnimation() - xOffset := updateCount % int(anim.FrameCount) * s.width + xOffset := updateCount % int(anim.TickCount) * s.width yOffset := int(anim.RowNumber) * s.height r := image.Rect(xOffset, yOffset, s.width, s.height).Intersect(s.imgData.Bounds()) @@ -74,9 +74,12 @@ func (s *Sprite) Image(ops imageOptions) image.Image { } func (s *Sprite) Draw(screen *ebiten.Image, options ...ImageOption) { + if s.getAnimation().RowNumber == 0 { + } + ops := imageOptions{ - scaleX: window.Scale, - scaleY: window.Scale, + scaleX: ux.Scale, + scaleY: ux.Scale, } for _, o := range options { @@ -98,11 +101,28 @@ func (s *Sprite) Draw(screen *ebiten.Image, options ...ImageOption) { ) } +func (s *Sprite) StartAnimation(id string, once bool) { + if a, ok := s.animations[id]; ok { + s.currentAnim = &a + } + if once { + s.animateOnce = true + s.animationStopAt = updateCount + int(s.currentAnim.TickCount) + } + s.animationStartedAt = updateCount +} + +func (s *Sprite) StopAnimation() { + s.currentAnim = nil + s.animationStartedAt = 0 + s.animationStopAt = 0 + s.animateOnce = false +} + type imageOptions struct { x, y float64 scaleX, scaleY float64 rotateTheta float64 - animation string } type ImageOption func(options *imageOptions) @@ -114,12 +134,6 @@ func ToScale(x, y float64) ImageOption { } } -func Animation(name string) ImageOption { - return func(o *imageOptions) { - o.animation = name - } -} - func AtPosition(x, y float64) ImageOption { return func(o *imageOptions) { o.x, o.y = x, y diff --git a/tools/spritedit/editor.go b/tools/spritedit/editor.go new file mode 100644 index 0000000..7f88726 --- /dev/null +++ b/tools/spritedit/editor.go @@ -0,0 +1,21 @@ +package main + +import "github.com/hajimehoshi/ebiten/v2" + +type editor struct { +} + +func (e *editor) Update() error { + //TODO implement me + panic("implement me") +} + +func (e *editor) Draw(screen *ebiten.Image) { + //TODO implement me + panic("implement me") +} + +func (e *editor) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + //TODO implement me + panic("implement me") +} diff --git a/tools/spritedit/main.go b/tools/spritedit/main.go index 4457d0b..16c432c 100644 --- a/tools/spritedit/main.go +++ b/tools/spritedit/main.go @@ -3,88 +3,13 @@ package main import ( "bytes" - "github.com/ebitenui/ebitenui" - "github.com/ebitenui/ebitenui/image" - "github.com/ebitenui/ebitenui/widget" "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/text/v2" - "golang.org/x/image/colornames" - "golang.org/x/image/font/gofont/goregular" ) -type Editor struct { - ui *ebitenui.UI -} - -func NewEditor() *Editor { - root := widget.NewContainer( - widget.ContainerOpts.BackgroundImage( - image.NewNineSliceColor(colornames.Gainsboro), - )) - root.AddChild( - widget.NewButton( - widget.ButtonOpts.TextLabel("Open Sprite Sheet..."), - widget.ButtonOpts.TextFace(DefaultFont()), - widget.ButtonOpts.TextColor(&widget.ButtonTextColor{ - Idle: colornames.Gainsboro, - Hover: colornames.Gainsboro, - Pressed: colornames.Gainsboro, - }), - widget.ButtonOpts.Image(&widget.ButtonImage{ - Idle: DefaultNineSlice(colornames.Darkslategray), - Hover: DefaultNineSlice(Mix(colornames.Darkslategray, colornames.Mediumseagreen, 0.4)), - Disabled: DefaultNineSlice(Mix(colornames.Darkslategray, colornames.Gainsboro, 0.8)), - Pressed: PressedNineSlice(Mix(colornames.Darkslategray, colornames.Black, 0.4)), - PressedHover: PressedNineSlice(Mix(colornames.Darkslategray, colornames.Black, 0.4)), - }), - widget.ButtonOpts.WidgetOpts( - widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ - VerticalPosition: widget.AnchorLayoutPositionCenter, - HorizontalPosition: widget.AnchorLayoutPositionCenter, - }), - widget.WidgetOpts.MinSize(180, 48), - ), - ), - ) - - return &Editor{ - ui: &ebitenui.UI{Container: root}, - } -} - -func (e *Editor) Update() error { - e.ui.Update() - return nil -} - -func (e *Editor) Draw(screen *ebiten.Image) { - e.ui.Draw(screen) -} - -func (e *Editor) Layout(w, h int) (int, int) { - return w, h -} - -func DefaultFont() *text.Face { - s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF)) - if err != nil { - panic(err) - } - - var f text.Face - - f = &text.GoTextFace{ - Source: s, - Size: 20, - } - - return &f -} - func main() { ebiten.SetWindowSize(480, 320) ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) - if err := ebiten.RunGame(NewEditor()); err != nil { + if err := ebiten.RunGame(); err != nil { panic(err) } } From 4829fbb5c7d0ef1b50030c2bf28d3319ceed5fbe Mon Sep 17 00:00:00 2001 From: Ben Vezzani Date: Thu, 28 Aug 2025 14:32:23 -0400 Subject: [PATCH 4/4] holy shit components are rendering whaaaaaaaaaaaaaaaat --- common/elements/v1/block.go | 28 ----- common/elements/v1/blocks/block.go | 58 +++++++++ common/elements/v1/blocks/options.go | 23 ++++ common/elements/v1/button.go | 140 ++++++++++----------- common/elements/v1/element.go | 59 ++------- common/elements/v1/mouse/mouse.go | 64 ++++++++++ common/elements/v1/stack.go | 31 ----- common/elements/v1/stacks/options.go | 26 ++++ common/elements/v1/stacks/stack.go | 51 ++++++++ common/elements/v1/table.go | 181 ++++++++++++++------------- tools/component_test/editor.go | 68 ++++++++++ tools/component_test/main.go | 15 +++ tools/spritedit/editor.go | 41 ++++-- tools/spritedit/main.go | 6 +- 14 files changed, 511 insertions(+), 280 deletions(-) delete mode 100644 common/elements/v1/block.go create mode 100644 common/elements/v1/blocks/block.go create mode 100644 common/elements/v1/blocks/options.go create mode 100644 common/elements/v1/mouse/mouse.go delete mode 100644 common/elements/v1/stack.go create mode 100644 common/elements/v1/stacks/options.go create mode 100644 common/elements/v1/stacks/stack.go create mode 100644 tools/component_test/editor.go create mode 100644 tools/component_test/main.go diff --git a/common/elements/v1/block.go b/common/elements/v1/block.go deleted file mode 100644 index 64d9a14..0000000 --- a/common/elements/v1/block.go +++ /dev/null @@ -1,28 +0,0 @@ -package elements - -import ( - "github.com/hajimehoshi/ebiten/v2" -) - -type BlockOption func(*block) - -func Block(ops ...BlockOption) ElementFunc { - return func(d Dimensions) Element { - b := block{} - for i := range ops { - ops[i](&b) - } - return &b - } -} - -type block struct { - width float64 - height float64 - xAlign xAlign - yAlign yAlign -} - -func (b block) Draw(image *ebiten.Image, anchorX, anchorY float64) (w, h float64) { - return b.width, b.height -} diff --git a/common/elements/v1/blocks/block.go b/common/elements/v1/blocks/block.go new file mode 100644 index 0000000..ade548e --- /dev/null +++ b/common/elements/v1/blocks/block.go @@ -0,0 +1,58 @@ +package blocks + +import ( + "image/color" + + "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/mouse" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +func New(ops ...Option) elements.ElementFunc { + return func(d elements.Bounds) elements.Element { + b := Block{ + ContainerBounds: d, + } + for i := range ops { + ops[i](&b) + } + return &b + } +} + +type Block struct { + ContainerBounds elements.Bounds + backgroundColor *color.Color + mouse.NopHandler + width float64 + height float64 + name string +} + +func (b *Block) Size() (w, h float64) { + w, h = b.ContainerBounds.Width, b.ContainerBounds.Height + if b.width != 0 { + w = b.width + } + if b.height != 0 { + h = b.height + } + return +} + +func (b *Block) Draw(image *ebiten.Image) (w, h float64) { + w, h = b.Size() + if b.backgroundColor != nil { + vector.DrawFilledRect( + image, + float32(b.ContainerBounds.Min.X), + float32(b.ContainerBounds.Min.Y), + float32(w), + float32(h), + *b.backgroundColor, + true, + ) + } + return +} diff --git a/common/elements/v1/blocks/options.go b/common/elements/v1/blocks/options.go new file mode 100644 index 0000000..352353c --- /dev/null +++ b/common/elements/v1/blocks/options.go @@ -0,0 +1,23 @@ +package blocks + +import "image/color" + +type Option func(*Block) + +func Size(w, h float64) Option { + return func(b *Block) { + b.width, b.height = w, h + } +} + +func BackgroundColor(c color.Color) Option { + return func(b *Block) { + b.backgroundColor = &c + } +} + +func Name(n string) Option { + return func(b *Block) { + b.name = n + } +} diff --git a/common/elements/v1/button.go b/common/elements/v1/button.go index 8a34c96..e59a809 100644 --- a/common/elements/v1/button.go +++ b/common/elements/v1/button.go @@ -1,74 +1,70 @@ package elements -import ( - "context" - "image/color" - - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/vector" - "golang.org/x/image/font" -) -import "git.vezzani.net/ben/games/common/ux/v1" - -type xAlign int -type yAlign int - -const ( - AlignCente xAlign = iota - AlignLeft - AlignRight -) - -const ( - AlignCenter yAlign = iota - AlignTop - AlignBottom -) - -type Button struct { - mouseHandler - block - Label string - OnClick func() error - OnRightClick func() error - Style struct { - MouseDownColor *color.Color - } -} - -func (b *Button) HandleClick(_ MouseState) error { - if b.OnClick == nil { - return nil - } - return b.OnClick() -} - -func (b *Button) getFont() font.Face { - if b.block.block.Font == nil { - return ux.FontFace - } - - return *b.block.block.Font -} - -func (b *Button) backgroundColor() color.Color { - var c *color.Color - if b.block.block.BackgroundColor != nil { - c = b.block.block.BackgroundColor - } else { - c = &ux.BackgroundColor - } - - if (b.mouseState.RightDown || b.mouseState.LeftDown) && b.Style.MouseDownColor != nil { - c = b.Style.MouseDownColor - } - - return *c -} - -func (b *Button) Draw(ctx context.Context, image *ebiten.Image) error { - - vector.StrokeRect(image, xz, yz, w, h, 1, b.backgroundColor(), true) - - return nil -} +// +//import ( +// "image/color" +//) +// +//type xAlign int +//type yAlign int +// +//const ( +// AlignCente xAlign = iota +// AlignLeft +// AlignRight +//) +// +//const ( +// AlignCenter yAlign = iota +// AlignTop +// AlignBottom +//) +// +//type Button struct { +// mouseHandler +// block +// Label string +// OnClick func() error +// OnRightClick func() error +// Style struct { +// MouseDownColor *color.Color +// } +//} +// +//func (b *Button) HandleClick(_ MouseState) error { +// if b.OnClick == nil { +// return nil +// } +// return b.OnClick() +//} +// +// +//func (b *Button) getFont() font.Face { +// if b.block.Font == nil { +// return ux.FontFace +// } +// +// return *b.block.block.Font +//} +// +//func (b *Button) backgroundColor() color.Color { +// var c *color.Color +// if b.block.block.BackgroundColor != nil { +// c = b.block.block.BackgroundColor +// } else { +// c = &ux.BackgroundColor +// } +// +// if (b.mouseState.RightDown || b.mouseState.LeftDown) && b.Style.MouseDownColor != nil { +// c = b.Style.MouseDownColor +// } +// +// return *c +//} +// +//func (b *Button) Draw(ctx context.Context, image *ebiten.Image) error { +// +// vector.StrokeRect(image, xz, yz, w, h, 1, b.backgroundColor(), true) +// +// return nil +//} diff --git a/common/elements/v1/element.go b/common/elements/v1/element.go index 8e28b6f..278fd90 100644 --- a/common/elements/v1/element.go +++ b/common/elements/v1/element.go @@ -1,68 +1,29 @@ package elements import ( + "git.vezzani.net/ben/games/common/elements/v1/mouse" "github.com/hajimehoshi/ebiten/v2" ) -type ElementFunc func(Dimensions) Element - -type MouseState struct { - X, Y int32 - LeftDown, RightDown bool - LeftChanged, RightChanged bool -} +type ElementFunc func(Bounds) Element type Clickable interface { - HandleClick(s MouseState) error } type Mouseable interface { - HandleMouseEnter(s MouseState) error - HandleMouseLeave(s MouseState) error - HandleMouseMove(s MouseState) error - HandleMouseDown(s MouseState) error - HandleMouseUp(s MouseState) error } type Element interface { - Draw(image *ebiten.Image, anchorX, anchorY float64) (w, h float64) + HandleMouseEvent(s mouse.State) bool + + Draw(image *ebiten.Image) (w, h float64) } -type Dimensions struct { - zx, zy float64 - wx, wy float64 +type Point struct { + X, Y float64 } -type mouseHandler struct { - mouseState MouseState - prevMouseState MouseState -} - -func (b *mouseHandler) HandleMouseEnter(s MouseState) error { - b.mouseState = s - return nil -} - -func (b *mouseHandler) HandleMouseLeave(s MouseState) error { - b.prevMouseState = b.mouseState - b.mouseState = MouseState{} - return nil -} - -func (b *mouseHandler) HandleMouseMove(s MouseState) error { - b.prevMouseState = b.mouseState - b.mouseState = s - return nil -} - -func (b *mouseHandler) HandleMouseDown(s MouseState) error { - b.prevMouseState = b.mouseState - b.mouseState = s - return nil -} - -func (b *mouseHandler) HandleMouseUp(s MouseState) error { - b.prevMouseState = b.mouseState - b.mouseState = s - return nil +type Bounds struct { + Min Point + Width, Height float64 } diff --git a/common/elements/v1/mouse/mouse.go b/common/elements/v1/mouse/mouse.go new file mode 100644 index 0000000..4a61d1d --- /dev/null +++ b/common/elements/v1/mouse/mouse.go @@ -0,0 +1,64 @@ +package mouse + +import ( + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +type NopHandler struct{} + +func (n NopHandler) HandleMouseEvent(s State) bool { + return false +} + +type State struct { + X, Y int + LeftDown, RightDown bool + LeftChanged, RightChanged bool + LeftClicked, RightClicked bool + Clicked bool +} + +//func ClickHandler(e elements.Element) func() bool { +// newState := StateBuilder() +// return func() bool { +// _ = newState() +// if +// } +//} + +func StateBuilder() func() State { + prevState := State{} + state := State{} + return func() State { + state = prevState + state.X, state.Y = ebiten.CursorPosition() + + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + state.LeftDown = true + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + state.RightDown = true + } + if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { + state.LeftDown = false + if !state.RightDown { + state.Clicked = true + } + } + if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) { + state.RightDown = false + if !state.LeftDown { + state.Clicked = true + } + } + + state.Clicked = state.Clicked && !prevState.Clicked + + state.LeftChanged = state.LeftDown != prevState.LeftDown + state.RightChanged = state.RightDown != prevState.RightDown + + prevState = state + return state + } +} diff --git a/common/elements/v1/stack.go b/common/elements/v1/stack.go deleted file mode 100644 index c7450eb..0000000 --- a/common/elements/v1/stack.go +++ /dev/null @@ -1,31 +0,0 @@ -package elements - -import "github.com/hajimehoshi/ebiten/v2" - -type StackOption func(*stack) - -func Stack(ops ...StackOption) ElementFunc { - return func(parentDimensions Dimensions) Element { - - } -} - -type stack struct { - block - horizontal bool - children []Element -} - -func (s *stack) Draw(image *ebiten.Image, anchorX, anchorY float64) (w, h float64) { - var offsetX, offsetY float64 - originalX, originalY := anchorX, anchorY - for i := range s.children { - offsetX, offsetY = s.children[i].Draw(image, anchorX, anchorY) - if s.horizontal { - anchorX += offsetX - } else { - anchorY += offsetY - } - } - return anchorX - originalX, anchorY - originalY -} diff --git a/common/elements/v1/stacks/options.go b/common/elements/v1/stacks/options.go new file mode 100644 index 0000000..8ed006b --- /dev/null +++ b/common/elements/v1/stacks/options.go @@ -0,0 +1,26 @@ +package stacks + +import ( + "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/blocks" +) + +type Option func(*Stack) + +func BlockOpt(o blocks.Option) Option { + return func(s *Stack) { + o(&s.Block) + } +} + +func Children(children ...elements.ElementFunc) Option { + return func(s *Stack) { + s.children = children + } +} + +func Horizontal() Option { + return func(s *Stack) { + s.horizontal = true + } +} diff --git a/common/elements/v1/stacks/stack.go b/common/elements/v1/stacks/stack.go new file mode 100644 index 0000000..012ffe4 --- /dev/null +++ b/common/elements/v1/stacks/stack.go @@ -0,0 +1,51 @@ +package stacks + +import ( + "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/blocks" + "git.vezzani.net/ben/games/common/elements/v1/mouse" + + "github.com/hajimehoshi/ebiten/v2" +) + +func New(ops ...Option) elements.ElementFunc { + return func(d elements.Bounds) elements.Element { + s := Stack{ + Block: blocks.Block{ContainerBounds: d}, + } + for op := range ops { + ops[op](&s) + } + return &s + } +} + +type Stack struct { + blocks.Block + mouse.NopHandler + horizontal bool + children []elements.ElementFunc +} + +func (s *Stack) Draw(image *ebiten.Image) (w, h float64) { + s.Block.Draw(image) + + d := s.ContainerBounds + d.Width, d.Height = s.Block.Size() + if s.horizontal { + d.Width /= float64(len(s.children)) + } else { + d.Height /= float64(len(s.children)) + } + + var offsetX, offsetY float64 + for i := range s.children { + offsetX, offsetY = s.children[i](d).Draw(image) + if s.horizontal { + d.Min.X += offsetX + } else { + d.Min.Y += offsetY + } + } + return s.Block.Size() +} diff --git a/common/elements/v1/table.go b/common/elements/v1/table.go index e546916..4250cef 100644 --- a/common/elements/v1/table.go +++ b/common/elements/v1/table.go @@ -1,92 +1,93 @@ package elements -import ( - "context" - "fmt" - - "github.com/hajimehoshi/ebiten/v2" -) - -type Table struct { - mouseHandler - block - - ColumnCount int - RowCount int - - Cells []Dimensions - - Style struct { - } - - mouseOverCell int -} - -func (t *Table) HandleClick(ctx context.Context, s MouseState) error { - address := t.getAddressUnderMouse() - if address < 0 || address >= len(t.Cells) { - return fmt.Errorf("cell address under mouse is out of bounds") - } - - if clickable, ok := t.Cells[address].(Clickable); ok { - return clickable.HandleClick(s) - } - - return nil -} - -func (t *Table) HandleMouseEnter(ctx context.Context, s MouseState) error { - _ = t.mouseHandler.HandleMouseEnter(s) - address := t.getAddressUnderMouse() - if address < 0 || address >= len(t.Cells) { - return fmt.Errorf("cell address under mouse is out of bounds") - } - - if mouseable, ok := t.Cells[address].(Mouseable); ok { - s.X -= - return mouseable.HandleMouseEnter(s) - } - - return nil -} - -func (t *Table) HandleMouseLeave(ctx context.Context, s MouseState) error { - _ = t.mouseHandler.HandleMouseLeave(s) - return nil -} - -func (t *Table) HandleMouseMove(ctx context.Context, s MouseState) error { - _ = t.mouseHandler.HandleMouseMove(s) - return nil -} - -func (t *Table) HandleMouseDown(ctx context.Context, s MouseState) error { - _ = t.mouseHandler.HandleMouseDown(s) - return nil -} - -func (t *Table) HandleMouseUp(ctx context.Context, s MouseState) error { - _ = t.mouseHandler.HandleMouseUp(s) - return nil -} - -func (t *Table) CellSize() (w, h float32) { - w, h = t. - return w / float32(t.ColumnCount), h / float32(t.RowCount) -} - -func (t *Table) CellZero(ctx context.Context, addr int) (x, y float32) { - x, y = GetZero(ctx) - w, h := t.CellSize(ctx) - - col, row := addr%t.ColumnCount, addr/t.ColumnCount - return x + float32(col)*w, y + float32(row) + h -} - -func (t *Table) Draw(screen *ebiten.Image, zx, zy float64) error { - return nil -} - -func (t *Table) getAddressUnderMouse() int { - return int(t.mouseState.Y)*t.RowCount + int(t.mouseState.X) -} +// +//import ( +// "context" +// "fmt" +// +// "github.com/hajimehoshi/ebiten/v2" +//) +// +//type Table struct { +// mouseHandler +// block +// +// ColumnCount int +// RowCount int +// +// Cells []Bounds +// +// Style struct { +// } +// +// mouseOverCell int +//} +// +//func (t *Table) HandleClick(ctx context.Context, s MouseState) error { +// address := t.getAddressUnderMouse() +// if address < 0 || address >= len(t.Cells) { +// return fmt.Errorf("cell address under mouse is out of bounds") +// } +// +// if clickable, ok := t.Cells[address].(Clickable); ok { +// return clickable.HandleClick(s) +// } +// +// return nil +//} +// +//func (t *Table) HandleMouseEnter(ctx context.Context, s MouseState) error { +// _ = t.mouseHandler.HandleMouseEnter(s) +// address := t.getAddressUnderMouse() +// if address < 0 || address >= len(t.Cells) { +// return fmt.Errorf("cell address under mouse is out of bounds") +// } +// +// if mouseable, ok := t.Cells[address].(Mouseable); ok { +// s.X -= +// return mouseable.HandleMouseEnter(s) +// } +// +// return nil +//} +// +//func (t *Table) HandleMouseLeave(ctx context.Context, s MouseState) error { +// _ = t.mouseHandler.HandleMouseLeave(s) +// return nil +//} +// +//func (t *Table) HandleMouseMove(ctx context.Context, s MouseState) error { +// _ = t.mouseHandler.HandleMouseMove(s) +// return nil +//} +// +//func (t *Table) HandleMouseDown(ctx context.Context, s MouseState) error { +// _ = t.mouseHandler.HandleMouseDown(s) +// return nil +//} +// +//func (t *Table) HandleMouseUp(ctx context.Context, s MouseState) error { +// _ = t.mouseHandler.HandleMouseUp(s) +// return nil +//} +// +//func (t *Table) CellSize() (w, h float32) { +// w, h = t. +// return w / float32(t.ColumnCount), h / float32(t.RowCount) +//} +// +//func (t *Table) CellZero(ctx context.Context, addr int) (x, y float32) { +// x, y = GetZero(ctx) +// w, h := t.CellSize(ctx) +// +// col, row := addr%t.ColumnCount, addr/t.ColumnCount +// return x + float32(col)*w, y + float32(row) + h +//} +// +//func (t *Table) Draw(screen *ebiten.Image, zx, zy float64) error { +// return nil +//} +// +//func (t *Table) getAddressUnderMouse() int { +// return int(t.mouseState.Y)*t.RowCount + int(t.mouseState.X) +//} diff --git a/tools/component_test/editor.go b/tools/component_test/editor.go new file mode 100644 index 0000000..eea5bb5 --- /dev/null +++ b/tools/component_test/editor.go @@ -0,0 +1,68 @@ +package main + +import ( + "image/color" + + "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/blocks" + "git.vezzani.net/ben/games/common/elements/v1/stacks" + "git.vezzani.net/ben/games/common/sprites/v1" + "github.com/hajimehoshi/ebiten/v2" + "golang.org/x/image/colornames" +) + +var root = stacks.New( + stacks.Horizontal(), + stacks.BlockOpt(blocks.BackgroundColor(color.White)), + stacks.BlockOpt(blocks.Size(200, 200)), + stacks.BlockOpt(blocks.Name("parent")), + stacks.Children( + stacks.New( + stacks.BlockOpt(blocks.Name("left")), + stacks.Children( + blocks.New(blocks.BackgroundColor(colornames.Green)), + blocks.New(blocks.BackgroundColor(colornames.Yellow)), + )), + stacks.New( + stacks.BlockOpt(blocks.Name("right")), + stacks.Children( + blocks.New(blocks.BackgroundColor(colornames.Blue)), + blocks.New(blocks.BackgroundColor(colornames.Red)), + ), + ), + ), +) + +func newEditor() *editor { + return &editor{} +} + +type editor struct { + root elements.Element + bounds elements.Bounds +} + +//var handleClick = mouse.ClickHandler(root) + +func (e *editor) Update() error { + sprites.Update() + //handleClick() + return nil +} + +func (e *editor) Draw(screen *ebiten.Image) { + e.root.Draw(screen) +} + +func (e *editor) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + if e.bounds.Width != float64(outsideWidth) || e.bounds.Height != float64(outsideHeight) { + e.bounds = elements.Bounds{ + Min: elements.Point{}, + Width: float64(outsideWidth), + Height: float64(outsideHeight), + } + e.root = root(e.bounds) + } + + return outsideWidth, outsideHeight +} diff --git a/tools/component_test/main.go b/tools/component_test/main.go new file mode 100644 index 0000000..a7d70bb --- /dev/null +++ b/tools/component_test/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/hajimehoshi/ebiten/v2" +) + +func main() { + ebiten.SetWindowSize(480, 320) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + ebiten.SetWindowTitle("Component/Layout Test") + e := newEditor() + if err := ebiten.RunGame(e); err != nil { + panic(err) + } +} diff --git a/tools/spritedit/editor.go b/tools/spritedit/editor.go index 7f88726..2dd65e1 100644 --- a/tools/spritedit/editor.go +++ b/tools/spritedit/editor.go @@ -1,21 +1,48 @@ package main -import "github.com/hajimehoshi/ebiten/v2" +import ( + "image/color" + + "git.vezzani.net/ben/games/common/elements/v1" + "git.vezzani.net/ben/games/common/elements/v1/blocks" + "git.vezzani.net/ben/games/common/elements/v1/stacks" + "git.vezzani.net/ben/games/common/sprites/v1" + "github.com/hajimehoshi/ebiten/v2" + "golang.org/x/image/colornames" +) + +var menu = stacks.New( + stacks.BlockOpt(blocks.BackgroundColor(color.White)), + stacks.BlockOpt(blocks.Size(100, 200)), + stacks.Children( + blocks.New(blocks.BackgroundColor(colornames.Green)), + blocks.New(), + ), +) + +func newEditor() *editor { + return &editor{} +} type editor struct { } func (e *editor) Update() error { - //TODO implement me - panic("implement me") + sprites.Update() + + return nil } func (e *editor) Draw(screen *ebiten.Image) { - //TODO implement me - panic("implement me") + b := screen.Bounds() + menu(elements.Bounds{ + ZX: float64(b.Min.X), + ZY: float64(b.Min.Y), + WX: float64(b.Max.X), + WY: float64(b.Max.Y), + }).Draw(screen) } func (e *editor) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { - //TODO implement me - panic("implement me") + return outsideWidth, outsideHeight } diff --git a/tools/spritedit/main.go b/tools/spritedit/main.go index 16c432c..530162d 100644 --- a/tools/spritedit/main.go +++ b/tools/spritedit/main.go @@ -1,15 +1,15 @@ package main import ( - "bytes" - "github.com/hajimehoshi/ebiten/v2" ) func main() { ebiten.SetWindowSize(480, 320) ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) - if err := ebiten.RunGame(); err != nil { + ebiten.SetWindowTitle("Sprite Editor") + e := newEditor() + if err := ebiten.RunGame(e); err != nil { panic(err) } }