Initial commit
implemented the game of life variation with hexagonal grid
This commit is contained in:
25
ui/colors.go
Normal file
25
ui/colors.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
)
|
||||
|
||||
func mixColors(a, b color.Color, percent float64) color.Color {
|
||||
rgba := func(c color.Color) (r, g, b, a uint8) {
|
||||
r16, g16, b16, a16 := c.RGBA()
|
||||
return uint8(r16 >> 8), uint8(g16 >> 8), uint8(b16 >> 8), uint8(a16 >> 8)
|
||||
}
|
||||
lerp := func(x, y uint8) uint8 {
|
||||
return uint8(math.Round(float64(x) + percent*(float64(y)-float64(x))))
|
||||
}
|
||||
r1, g1, b1, a1 := rgba(a)
|
||||
r2, g2, b2, a2 := rgba(b)
|
||||
|
||||
return color.RGBA{
|
||||
R: lerp(r1, r2),
|
||||
G: lerp(g1, g2),
|
||||
B: lerp(b1, b2),
|
||||
A: lerp(a1, a2),
|
||||
}
|
||||
}
|
||||
58
ui/event_bus.go
Normal file
58
ui/event_bus.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/nmoniz/gubgub"
|
||||
)
|
||||
|
||||
var eventBusGetter = sync.OnceValue(func() *eventBus {
|
||||
return NewSyncEventBus()
|
||||
})
|
||||
|
||||
func EventBus() *eventBus {
|
||||
return eventBusGetter()
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
Name() string
|
||||
Data() any
|
||||
}
|
||||
|
||||
type eventBus struct {
|
||||
topics map[string]gubgub.Topic[Event]
|
||||
}
|
||||
|
||||
func NewSyncEventBus() *eventBus {
|
||||
return &eventBus{
|
||||
topics: make(map[string]gubgub.Topic[Event]),
|
||||
}
|
||||
}
|
||||
|
||||
func (eb *eventBus) Emit(e Event) error {
|
||||
topic, ok := eb.topics[e.Name()]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
err := topic.Publish(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("emit event: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (eb *eventBus) Listen(name string, handler func(Event)) error {
|
||||
topic, ok := eb.topics[name]
|
||||
if !ok {
|
||||
topic = gubgub.NewSyncTopic[Event]()
|
||||
eb.topics[name] = topic
|
||||
}
|
||||
|
||||
err := topic.Subscribe(gubgub.Forever(handler))
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen for event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
9
ui/events.go
Normal file
9
ui/events.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package ui
|
||||
|
||||
const (
|
||||
ResetButtonEvent = "reset_button"
|
||||
SpeedSliderEvent = "speed_slider"
|
||||
ReproductionTargetEvent = "reproduction_target_slider"
|
||||
OverPopulationSliderEvent = "over_population_slider"
|
||||
UnderPopulationSliderEvent = "under_population_slider"
|
||||
)
|
||||
365
ui/root.go
Normal file
365
ui/root.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
|
||||
"github.com/ebitenui/ebitenui/image"
|
||||
"github.com/ebitenui/ebitenui/widget"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"golang.org/x/image/colornames"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
)
|
||||
|
||||
func Root() *widget.Container {
|
||||
face, err := loadFont(12)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
slidersTrack := &widget.SliderTrackImage{
|
||||
Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
|
||||
Hover: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
|
||||
}
|
||||
|
||||
slidersButton := &widget.ButtonImage{
|
||||
Idle: image.NewNineSliceColor(color.NRGBA{255, 100, 100, 255}),
|
||||
Hover: image.NewNineSliceColor(color.NRGBA{255, 100, 100, 255}),
|
||||
Pressed: image.NewNineSliceColor(color.NRGBA{255, 100, 100, 255}),
|
||||
}
|
||||
|
||||
root := widget.NewContainer(
|
||||
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
|
||||
)
|
||||
|
||||
topLeft := widget.NewContainer(
|
||||
// widget.ContainerOpts.BackgroundImage(
|
||||
// image.NewNineSliceColor(colornames.Indigo),
|
||||
// ),
|
||||
widget.ContainerOpts.Layout(widget.NewRowLayout(
|
||||
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
|
||||
widget.RowLayoutOpts.Spacing(5),
|
||||
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(5)),
|
||||
)),
|
||||
widget.ContainerOpts.WidgetOpts(
|
||||
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||
VerticalPosition: widget.AnchorLayoutPositionStart,
|
||||
HorizontalPosition: widget.AnchorLayoutPositionStart,
|
||||
StretchVertical: true,
|
||||
}),
|
||||
widget.WidgetOpts.MinSize(80, 50),
|
||||
),
|
||||
)
|
||||
|
||||
root.AddChild(topLeft)
|
||||
|
||||
overPopLabel := widget.NewText(
|
||||
widget.TextOpts.Text(fmt.Sprintf("Over Population Threshold: >%d", 5), &face, color.White),
|
||||
widget.TextOpts.Position(widget.TextPositionStart, widget.TextPositionCenter),
|
||||
widget.TextOpts.WidgetOpts(
|
||||
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||
Position: widget.RowLayoutPositionCenter,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
topLeft.AddChild(overPopLabel)
|
||||
|
||||
overPopSlider := widget.NewSlider(
|
||||
// Set the slider orientation - n/s vs e/w
|
||||
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||
// Set the minimum and maximum value for the slider
|
||||
widget.SliderOpts.MinMax(0, 6),
|
||||
// Set the current value of the slider, without triggering a change event
|
||||
widget.SliderOpts.InitialCurrent(5),
|
||||
widget.SliderOpts.WidgetOpts(
|
||||
// Set the Widget to layout in the center on the screen
|
||||
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||
HorizontalPosition: widget.AnchorLayoutPositionCenter,
|
||||
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||
}),
|
||||
// Set the widget's dimensions
|
||||
widget.WidgetOpts.MinSize(100, 6),
|
||||
),
|
||||
widget.SliderOpts.Images(
|
||||
slidersTrack,
|
||||
slidersButton,
|
||||
),
|
||||
// Set the size of the handle
|
||||
widget.SliderOpts.FixedHandleSize(6),
|
||||
// Set the offset to display the track
|
||||
widget.SliderOpts.TrackOffset(0),
|
||||
// Set the size to move the handle
|
||||
widget.SliderOpts.PageSizeFunc(func() int {
|
||||
return 1
|
||||
}),
|
||||
)
|
||||
topLeft.AddChild(overPopSlider)
|
||||
|
||||
underPopLabel := widget.NewText(
|
||||
widget.TextOpts.Text(fmt.Sprintf("Under Population Threshold: <%d", 5), &face, color.White),
|
||||
widget.TextOpts.Position(widget.TextPositionStart, widget.TextPositionCenter),
|
||||
widget.TextOpts.WidgetOpts(
|
||||
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||
Position: widget.RowLayoutPositionCenter,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
topLeft.AddChild(underPopLabel)
|
||||
|
||||
underPopSlider := widget.NewSlider(
|
||||
// Set the slider orientation - n/s vs e/w
|
||||
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||
// Set the minimum and maximum value for the slider
|
||||
widget.SliderOpts.MinMax(0, 6),
|
||||
// Set the current value of the slider, without triggering a change event
|
||||
widget.SliderOpts.InitialCurrent(2),
|
||||
widget.SliderOpts.WidgetOpts(
|
||||
// Set the Widget to layout in the center on the screen
|
||||
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||
HorizontalPosition: widget.AnchorLayoutPositionCenter,
|
||||
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||
}),
|
||||
// Set the widget's dimensions
|
||||
widget.WidgetOpts.MinSize(100, 6),
|
||||
),
|
||||
widget.SliderOpts.Images(
|
||||
slidersTrack,
|
||||
slidersButton,
|
||||
),
|
||||
// Set the size of the handle
|
||||
widget.SliderOpts.FixedHandleSize(6),
|
||||
// Set the offset to display the track
|
||||
widget.SliderOpts.TrackOffset(0),
|
||||
// Set the size to move the handle
|
||||
widget.SliderOpts.PageSizeFunc(func() int {
|
||||
return 1
|
||||
}),
|
||||
// Set the callback to call when the slider value is changed
|
||||
widget.SliderOpts.ChangedHandler(func(args *widget.SliderChangedEventArgs) {
|
||||
if args.Current == 0 {
|
||||
underPopLabel.Label = "Under Population Threshold: (disabled)"
|
||||
} else {
|
||||
underPopLabel.Label = fmt.Sprintf("Under Population Threshold: <%d", args.Current)
|
||||
}
|
||||
if args.Current > overPopSlider.Current {
|
||||
overPopSlider.Current = args.Current
|
||||
}
|
||||
_ = EventBus().Emit(SliderEvent{data: args, name: UnderPopulationSliderEvent})
|
||||
}),
|
||||
)
|
||||
|
||||
overPopSlider.ChangedEvent.AddHandler(func(argsRaw any) {
|
||||
args, ok := argsRaw.(*widget.SliderChangedEventArgs)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if args.Current == 6 {
|
||||
overPopLabel.Label = "Over Population Threshold: (disabled)"
|
||||
} else {
|
||||
overPopLabel.Label = fmt.Sprintf("Over Population Threshold: >%d ", args.Current)
|
||||
}
|
||||
|
||||
if args.Current < underPopSlider.Current {
|
||||
underPopSlider.Current = args.Current
|
||||
}
|
||||
_ = EventBus().Emit(SliderEvent{data: args, name: OverPopulationSliderEvent})
|
||||
})
|
||||
|
||||
topLeft.AddChild(underPopSlider)
|
||||
|
||||
reprodTartgeLabel := widget.NewText(
|
||||
widget.TextOpts.Text(fmt.Sprintf("Reproduction Target: =%d", 2), &face, color.White),
|
||||
widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter),
|
||||
widget.TextOpts.WidgetOpts(
|
||||
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||
Position: widget.RowLayoutPositionStart,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
topLeft.AddChild(reprodTartgeLabel)
|
||||
|
||||
reprodTargetSlider := widget.NewSlider(
|
||||
// Set the slider orientation - n/s vs e/w
|
||||
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||
// Set the minimum and maximum value for the slider
|
||||
widget.SliderOpts.MinMax(0, 6),
|
||||
// Set the current value of the slider, without triggering a change event
|
||||
widget.SliderOpts.InitialCurrent(2),
|
||||
widget.SliderOpts.WidgetOpts(
|
||||
// Set the Widget to layout in the center on the screen
|
||||
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||
HorizontalPosition: widget.AnchorLayoutPositionStart,
|
||||
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||
}),
|
||||
// Set the widget's dimensions
|
||||
widget.WidgetOpts.MinSize(100, 6),
|
||||
),
|
||||
widget.SliderOpts.Images(
|
||||
slidersTrack,
|
||||
slidersButton,
|
||||
),
|
||||
// Set the size of the handle
|
||||
widget.SliderOpts.FixedHandleSize(6),
|
||||
// Set the offset to display the track
|
||||
//
|
||||
widget.SliderOpts.TrackOffset(0),
|
||||
// Set the size to move the handle
|
||||
widget.SliderOpts.PageSizeFunc(func() int {
|
||||
return 1
|
||||
}),
|
||||
// Set the callback to call when the slider value is changed
|
||||
widget.SliderOpts.ChangedHandler(func(args *widget.SliderChangedEventArgs) {
|
||||
if args.Current > overPopSlider.Current || args.Current < underPopSlider.Current {
|
||||
reprodTartgeLabel.Label = "Reproduction target: (disabled due to other parameters)"
|
||||
} else {
|
||||
reprodTartgeLabel.Label = fmt.Sprintf("Reproduction target: =%d", args.Current)
|
||||
}
|
||||
_ = EventBus().Emit(SliderEvent{data: args, name: ReproductionTargetEvent})
|
||||
}),
|
||||
)
|
||||
|
||||
topLeft.AddChild(reprodTargetSlider)
|
||||
|
||||
bottomRight := widget.NewContainer(
|
||||
widget.ContainerOpts.Layout(widget.NewRowLayout(
|
||||
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
|
||||
widget.RowLayoutOpts.Spacing(5),
|
||||
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(5)),
|
||||
)),
|
||||
widget.ContainerOpts.WidgetOpts(
|
||||
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||
VerticalPosition: widget.AnchorLayoutPositionEnd,
|
||||
HorizontalPosition: widget.AnchorLayoutPositionEnd,
|
||||
}),
|
||||
widget.WidgetOpts.MinSize(80, 50),
|
||||
),
|
||||
)
|
||||
|
||||
root.AddChild(bottomRight)
|
||||
|
||||
speedLabel := widget.NewText(
|
||||
widget.TextOpts.Text("Speed: 0 ticks/second", &face, color.White),
|
||||
widget.TextOpts.Position(widget.TextPositionStart, widget.TextPositionCenter),
|
||||
widget.TextOpts.WidgetOpts(
|
||||
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||
Position: widget.RowLayoutPositionCenter,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
bottomRight.AddChild(speedLabel)
|
||||
|
||||
speedSlider := widget.NewSlider(
|
||||
// Set the slider orientation - n/s vs e/w
|
||||
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||
// Set the minimum and maximum value for the slider
|
||||
widget.SliderOpts.MinMax(0, 10),
|
||||
// Set the current value of the slider, without triggering a change event
|
||||
widget.SliderOpts.InitialCurrent(1),
|
||||
widget.SliderOpts.WidgetOpts(
|
||||
// Set the Widget to layout in the center on the screen
|
||||
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||
HorizontalPosition: widget.AnchorLayoutPositionCenter,
|
||||
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||
}),
|
||||
// Set the widget's dimensions
|
||||
widget.WidgetOpts.MinSize(100, 6),
|
||||
),
|
||||
widget.SliderOpts.Images(
|
||||
slidersTrack,
|
||||
slidersButton,
|
||||
),
|
||||
// Set the size of the handle
|
||||
widget.SliderOpts.FixedHandleSize(6),
|
||||
// Set the offset to display the track
|
||||
widget.SliderOpts.TrackOffset(0),
|
||||
// Set the size to move the handle
|
||||
widget.SliderOpts.PageSizeFunc(func() int {
|
||||
return 1
|
||||
}),
|
||||
// Set the callback to call when the slider value is changed
|
||||
widget.SliderOpts.ChangedHandler(func(args *widget.SliderChangedEventArgs) {
|
||||
speedLabel.Label = fmt.Sprintf("Speed: %d ticks/second", args.Current)
|
||||
_ = EventBus().Emit(SliderEvent{data: args, name: SpeedSliderEvent})
|
||||
}),
|
||||
)
|
||||
|
||||
bottomRight.AddChild(speedSlider)
|
||||
|
||||
baseColor := colornames.Aliceblue
|
||||
resetButton := widget.NewButton(
|
||||
widget.ButtonOpts.Image(&widget.ButtonImage{
|
||||
Idle: image.NewBorderedNineSliceColor(baseColor, colornames.Black, 1),
|
||||
Hover: image.NewBorderedNineSliceColor(mixColors(baseColor, colornames.Gainsboro, 0.2), colornames.Black, 1),
|
||||
Pressed: image.NewBorderedNineSliceColor(mixColors(baseColor, colornames.Darkgray, 0.2), colornames.Black, 1),
|
||||
}),
|
||||
widget.ButtonOpts.Text("Reset", &face, &widget.ButtonTextColor{
|
||||
Idle: colornames.Purple,
|
||||
}),
|
||||
widget.ButtonOpts.WidgetOpts(
|
||||
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||
Stretch: true,
|
||||
}),
|
||||
),
|
||||
widget.ButtonOpts.TextPosition(
|
||||
widget.TextPositionCenter,
|
||||
widget.TextPositionCenter,
|
||||
),
|
||||
widget.ButtonOpts.ClickedHandler(func(args *widget.ButtonClickedEventArgs) {
|
||||
_ = EventBus().Emit(ButtonEvent{data: args, name: ResetButtonEvent})
|
||||
}),
|
||||
)
|
||||
|
||||
bottomRight.AddChild(resetButton)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
type SliderEvent struct {
|
||||
name string
|
||||
data *widget.SliderChangedEventArgs
|
||||
}
|
||||
|
||||
// Data implements Event.
|
||||
func (s SliderEvent) Data() any {
|
||||
return s.data
|
||||
}
|
||||
|
||||
// Name implements Event.
|
||||
func (s SliderEvent) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
type ButtonEvent struct {
|
||||
name string
|
||||
data *widget.ButtonClickedEventArgs
|
||||
}
|
||||
|
||||
// Data implements Event.
|
||||
func (b ButtonEvent) Data() any {
|
||||
return b.data
|
||||
}
|
||||
|
||||
// Name implements Event.
|
||||
func (b ButtonEvent) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
func loadFont(size float64) (text.Face, error) {
|
||||
s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &text.GoTextFace{
|
||||
Source: s,
|
||||
Size: size,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user