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) + } +}