diff --git a/TODO.md b/TODO.md index 7610887..77fa3dd 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ - [ ] Add more unit tests? - [X] Fix z-fighting of monsters. - [X] Add exploding animation of monsters. -- [ ] Add audio settings (music & sound volume). +- [X] Add audio settings (music & sound volume). - [X] Hearts must be saved as well for resume. - [ ] Add demo mode. - [ ] Add touch controls diff --git a/cmd/tins2021/appcontext.go b/cmd/tins2021/appcontext.go index 2038739..c6b1769 100644 --- a/cmd/tins2021/appcontext.go +++ b/cmd/tins2021/appcontext.go @@ -41,6 +41,9 @@ func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021. DyingMonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{}, } + app.Audio.SampleVolume = settings.Audio.SoundVolume + app.Audio.MusicVolume = settings.Audio.MusicVolume + monsterNames := map[tins2021.MonsterType]string{ tins2021.MonsterTypeStraight: "straight-walking", tins2021.MonsterTypeRandom: "random-walking", @@ -83,6 +86,33 @@ func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) { controller.Play(level) } +func (app *appContext) playNextGameMusic(ctx ui.Context) { + if app.GameMusic != nil { + return + } + const songs = 4 + pick := func() int { + for { + s := rand.Intn(songs) + 1 + if s == app.GameMusicSong { + continue + } + app.GameMusicSong = s + return s + } + } + song := fmt.Sprintf("song_game_%d.mp3", pick()) + app.GameMusic, _ = app.Audio.PlayMusic(song, func(m *Music) { + m.OnFinished = func() { + if app.GameMusic == nil { + return + } + app.GameMusic = nil + app.playNextGameMusic(ctx) + } + }) +} + func (app *appContext) PlayResume(ctx ui.Context) { level := tins2021.NewLevel() level.Score = app.Score.Current.Score @@ -131,31 +161,22 @@ func (app *appContext) ShowMainMenu(ctx ui.Context) { app.show(ctx, newMainMenu(app, ctx)) } -func (app *appContext) playNextGameMusic(ctx ui.Context) { - if app.GameMusic != nil { - return +func (app *appContext) setMusicVolume(volume float64) { + app.Settings.Audio.MusicVolume = volume + app.Audio.MusicVolume = volume + menu := app.MenuMusic + if menu != nil { + menu.Volume.Volume = volume } - const songs = 4 - pick := func() int { - for { - s := rand.Intn(songs) + 1 - if s == app.GameMusicSong { - continue - } - app.GameMusicSong = s - return s - } + game := app.GameMusic + if game != nil { + game.Volume.Volume = volume } - song := fmt.Sprintf("song_game_%d.mp3", pick()) - app.GameMusic, _ = app.Audio.PlayMusic(song, func(m *Music) { - m.OnFinished = func() { - if app.GameMusic == nil { - return - } - app.GameMusic = nil - app.playNextGameMusic(ctx) - } - }) +} + +func (app *appContext) setSoundVolume(volume float64) { + app.Settings.Audio.SoundVolume = volume + app.Audio.SampleVolume = volume } func (app *appContext) switchToPlayMusic(ctx ui.Context) { diff --git a/cmd/tins2021/audio.go b/cmd/tins2021/audio.go index e253101..062319c 100644 --- a/cmd/tins2021/audio.go +++ b/cmd/tins2021/audio.go @@ -35,8 +35,8 @@ func NewAudioPlayer(resources ui.Resources, prefix string) *AudioPlayer { prefix: prefix, SampleRate: rate, Samples: map[string]Sample{}, - SampleVolume: 1, - MusicVolume: 1, + SampleVolume: 0, + MusicVolume: 0, } } @@ -83,8 +83,8 @@ func (p *AudioPlayer) PlaySample(name string) error { speaker.Play(&effects.Volume{ Streamer: p.resample(sample.Stream(), sample.SampleRate), Base: 2, - Volume: float64(sample.Volume), - Silent: false, + Volume: p.SampleVolume - sample.Volume, + Silent: p.SampleVolume == minVolume, }) return nil } @@ -106,8 +106,8 @@ func (p *AudioPlayer) PlayMusic(name string, init func(*Music)) (*Music, error) Volume: &effects.Volume{ Streamer: p.resample(closer, format.SampleRate), Base: 2, - Volume: 1, - Silent: false, + Volume: p.MusicVolume, + Silent: p.MusicVolume == minVolume, }, } if init != nil { diff --git a/cmd/tins2021/mainmenu.go b/cmd/tins2021/mainmenu.go index 91c55ba..3a0a3e3 100644 --- a/cmd/tins2021/mainmenu.go +++ b/cmd/tins2021/mainmenu.go @@ -44,7 +44,7 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control { menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) }) } menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) }) - menu.Add("Controls", func(ctx ui.Context) { app.ShowSettings(ctx) }) + menu.Add("Settings", func(ctx ui.Context) { app.ShowSettings(ctx) }) menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) }) menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() }) diff --git a/cmd/tins2021/settings.go b/cmd/tins2021/settings.go index 3c4c5dd..67f07de 100644 --- a/cmd/tins2021/settings.go +++ b/cmd/tins2021/settings.go @@ -6,6 +6,7 @@ import ( "image/color" "github.com/golang/freetype/truetype" + "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dimg" "opslag.de/schobers/geom" "opslag.de/schobers/tins2021" @@ -13,6 +14,12 @@ import ( "opslag.de/schobers/zntg/ui" ) +const ( + controlsTypeWASD = "wasd" + controlsTypeArrows = "arrows" + controlsTypeCustom = "custom" +) + const keyboardKeyCornerRadius = .1 * keyboardKeyWidth const keyboardKeyHeight = .2 * keyboardLayoutTextureWidth const keyboardKeySkew = .15 @@ -20,6 +27,14 @@ const keyboardKeyWidth = .25 * keyboardLayoutTextureWidth const keyboardLayoutTextureHeight = 256 const keyboardLayoutTextureWidth = 2 * keyboardLayoutTextureHeight +const maxVolume = 2 +const minVolume = -8 + +const volumeControlBarWidth = 154 +const volumeControlKnobWidth = 154 +const volumeControlTextureHeight = 256 +const volumeControlTextureWidth = 2*volumeControlKnobWidth + 2*volumeControlBarWidth + func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.PointF, key rune, color color.Color) { const cornerRadius = keyboardKeyCornerRadius const keyHeight_5 = .5 * keyboardKeyHeight @@ -115,6 +130,68 @@ func generateKeys(resources ui.Resources, keys ...keyboardLayoutKey) image.Image return im } +func generateVolumeControlTexture() (image.Image, map[string]geom.RectangleF) { + im := image.NewRGBA(image.Rect(0, 0, volumeControlTextureWidth, volumeControlTextureHeight)) + ctx := draw2dimg.NewGraphicContext(im) + + const unitMultiplier = float64(volumeControlTextureHeight) + var left float64 + coord := func(x, y float64) (float64, float64) { + return left + x*unitMultiplier, y * unitMultiplier + } + + regions := map[string]geom.RectangleF{} + ctx.SetFillColor(color.White) + ctx.SetStrokeColor(color.White) + ctx.SetLineCap(draw2d.SquareCap) + + ctx.SetLineWidth(16) + ctx.MoveTo(coord(.1, .5)) + ctx.LineTo(coord(.4, .2)) + ctx.LineTo(coord(.52, .32)) + ctx.LineTo(coord(.52, .68)) + ctx.LineTo(coord(.4, .8)) + ctx.Close() + ctx.Stroke() + regions["leftKnob"] = geom.RectF(0, 0, volumeControlKnobWidth, volumeControlTextureHeight) + + left += volumeControlKnobWidth + ctx.SetLineWidth(16) + ctx.MoveTo(coord(.1, .2)) + ctx.LineTo(coord(.3, .0)) + ctx.LineTo(coord(.5, .2)) + ctx.LineTo(coord(.5, .8)) + ctx.LineTo(coord(.3, 1)) + ctx.LineTo(coord(.1, .8)) + ctx.Close() + ctx.Stroke() + regions["bar"] = geom.RectF(left, 0, left+volumeControlBarWidth, volumeControlTextureHeight) + + left += volumeControlBarWidth + ctx.MoveTo(coord(.1, .2)) + ctx.LineTo(coord(.3, .0)) + ctx.LineTo(coord(.5, .2)) + ctx.LineTo(coord(.5, .8)) + ctx.LineTo(coord(.3, 1)) + ctx.LineTo(coord(.1, .8)) + ctx.Close() + ctx.FillStroke() + regions["barFilled"] = geom.RectF(left, 0, left+volumeControlBarWidth, volumeControlTextureHeight) + + left += volumeControlBarWidth + ctx.SetLineWidth(16) + ctx.MoveTo(coord(.5, .5)) + ctx.LineTo(coord(.2, .2)) + ctx.LineTo(coord(.08, .32)) + ctx.LineTo(coord(.08, .68)) + ctx.LineTo(coord(.2, .8)) + ctx.Close() + ctx.Stroke() + regions["rightKnob"] = geom.RectF(left, 0, left+volumeControlKnobWidth, volumeControlTextureHeight) + + return im, regions +} + func generateWASDKeys(resources ui.Resources) image.Image { return generateKeys(resources, keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: 'W'}, @@ -130,24 +207,19 @@ type keyboardLayoutKey struct { Highlight bool } -type settings struct { - ui.StackPanel +type keyboardLayoutSettings struct { + ui.ControlBase app *appContext + Active bool ActiveLayout int SelectedLayout int SelectingCustom int } -const ( - controlsTypeWASD = "wasd" - controlsTypeArrows = "arrows" - controlsTypeCustom = "custom" -) - -func newSettings(app *appContext, ctx ui.Context) *settings { +func newKeyboardLayoutSettings(app *appContext, ctx ui.Context) *keyboardLayoutSettings { ctx.Textures().CreateTextureGo("layout-wasd", generateWASDKeys(ctx.Resources()), true) ctx.Textures().CreateTextureGo("layout-arrows", generateArrowKeys(ctx.Resources()), true) ctx.Textures().CreateTextureGo("layout-select-1", generateArrowKeysHighlight(ctx.Resources(), [4]bool{true, false, false, false}), true) @@ -163,42 +235,19 @@ func newSettings(app *appContext, ctx ui.Context) *settings { layout = 2 } - settings := &settings{app: app, ActiveLayout: layout, SelectedLayout: layout} + settings := &keyboardLayoutSettings{app: app, ActiveLayout: layout, SelectedLayout: layout} settings.renderCustomLayout(ctx) return settings } -var supportedCustomKeys = map[ui.Key]string{ - ui.KeyA: "A", - ui.KeyB: "B", - ui.KeyC: "C", - ui.KeyD: "D", - ui.KeyE: "E", - ui.KeyF: "F", - ui.KeyG: "G", - ui.KeyH: "H", - ui.KeyI: "I", - ui.KeyJ: "J", - ui.KeyK: "K", - ui.KeyL: "L", - ui.KeyM: "M", - ui.KeyN: "N", - ui.KeyO: "O", - ui.KeyP: "P", - ui.KeyQ: "Q", - ui.KeyR: "R", - ui.KeyS: "S", - ui.KeyT: "T", - ui.KeyU: "U", - ui.KeyV: "V", - ui.KeyW: "W", - ui.KeyX: "X", - ui.KeyY: "Y", - ui.KeyZ: "Z", +func (s *keyboardLayoutSettings) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { + scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*size.X) + font := ctx.Fonts().Font("default") + return geom.PtF32(geom.NaN32(), 2*font.Height()+scale*keyboardLayoutTextureHeight) } -func (s *settings) Handle(ctx ui.Context, e ui.Event) bool { - if s.StackPanel.Handle(ctx, e) { +func (s *keyboardLayoutSettings) Handle(ctx ui.Context, e ui.Event) bool { + if s.ControlBase.Handle(ctx, e) { return true } switch e := e.(type) { @@ -241,11 +290,17 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool { s.app.MenuInteraction() return true case ui.KeyLeft: - s.setActiveLayout(s.ActiveLayout - 1) + if s.Active { + s.setActiveLayout(s.ActiveLayout - 1) + } case ui.KeyRight: - s.setActiveLayout(s.ActiveLayout + 1) + if s.Active { + s.setActiveLayout(s.ActiveLayout + 1) + } case ui.KeyEnter: - s.selectLayout() + if s.Active { + s.selectLayout() + } } case *ui.MouseMoveEvent: if s.SelectingCustom == 0 { @@ -254,6 +309,7 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool { s.setActiveLayout(layout) } } + s.Active = s.IsOver() case *ui.MouseButtonDownEvent: if s.SelectingCustom == 0 { if e.Button == ui.MouseButtonLeft { @@ -268,34 +324,20 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool { return false } -func (s *settings) selectLayout() { - s.app.MenuInteraction() - switch s.ActiveLayout { - case 0: - s.SelectedLayout = 0 - s.app.Settings.Controls.Type = controlsTypeWASD - case 1: - s.SelectedLayout = 1 - s.app.Settings.Controls.Type = controlsTypeArrows - case 2: - s.SelectingCustom = 1 - } -} - -func (s *settings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int { +func (s *keyboardLayoutSettings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int { bounds := s.Bounds() - center := bounds.Center() width := bounds.Dx() scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width) font := ctx.Fonts().Font("default") + top := bounds.Min.Y + bottom := top + 2*font.Height() + scale*keyboardLayoutTextureHeight + for i := 0; i < 3; i++ { left := (.04 + .32*float32(i)) * width right := left + .28*width - top := center.Y - 2*font.Height() - bottom := center.Y + scale*keyboardLayoutTextureWidth if mouse.In(geom.RectF32(left, top, right, bottom)) { return i } @@ -303,23 +345,37 @@ func (s *settings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int { return -1 } -func (s *settings) setActiveLayout(layout int) { - layout = (layout + 3) % 3 - change := layout != s.ActiveLayout - s.ActiveLayout = (layout + 3) % 3 - if change { - s.app.MenuInteraction() - } -} - -func (s *settings) Render(ctx ui.Context) { +func (s *keyboardLayoutSettings) PostRender(ctx ui.Context) { bounds := s.Bounds() - center := bounds.Center() width := bounds.Dx() renderer := ctx.Renderer() scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width) + font := ctx.Fonts().Font("default") + normalColor := ctx.Style().Palette.Text + + top := bounds.Min.Y + layoutTop := top + 2*font.Height() + + if s.SelectingCustom > 0 { + renderer.FillRectangle(geom.ZeroPtF32.RectRel(renderer.Size().ToF32()), zntg.MustHexColor(`#000000DF`)) + + selectTexture := fmt.Sprintf("layout-select-%d", s.SelectingCustom) + + layoutLeft := .36 * width + layoutCenter := layoutLeft + .14*width + renderer.TextAlign(font, geom.PtF32(layoutCenter, top), normalColor, "PRESS KEY TO ASSIGN", ui.AlignCenter) + renderer.DrawTexturePoint(ctx.Textures().ScaledByName(selectTexture, scale), geom.PtF32(layoutLeft, layoutTop)) + } +} + +func (s *keyboardLayoutSettings) Render(ctx ui.Context) { + bounds := s.Bounds() + width := bounds.Dx() + renderer := ctx.Renderer() + + scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width) font := ctx.Fonts().Font("default") layouts := []string{ @@ -332,6 +388,8 @@ func (s *settings) Render(ctx ui.Context) { normalColor := ctx.Style().Palette.Text highlightColor := ctx.Style().Palette.Primary + top := bounds.Min.Y + layoutTop := top + 2*font.Height() for i, layout := range layouts { layoutLeft := (.04 + .32*float32(i)) * width layoutCenter := layoutLeft + .14*width @@ -345,23 +403,12 @@ func (s *settings) Render(ctx ui.Context) { layoutColor = highlightColor } - renderer.TextAlign(font, geom.PtF32(layoutCenter, center.Y-2*font.Height()), textColor, layout, ui.AlignCenter) - renderer.DrawTexturePointOptions(ctx.Textures().ScaledByName(layoutTextures[i], scale), geom.PtF32(layoutLeft, center.Y), ui.DrawOptions{Tint: layoutColor}) - } - - if s.SelectingCustom > 0 { - renderer.FillRectangle(bounds, zntg.MustHexColor(`#000000DF`)) - - selectTexture := fmt.Sprintf("layout-select-%d", s.SelectingCustom) - - layoutLeft := .36 * width - layoutCenter := layoutLeft + .14*width - renderer.TextAlign(font, geom.PtF32(layoutCenter, center.Y-2*font.Height()), normalColor, "PRESS KEY TO ASSIGN", ui.AlignCenter) - renderer.DrawTexturePoint(ctx.Textures().ScaledByName(selectTexture, scale), geom.PtF32(layoutLeft, center.Y)) + renderer.TextAlign(font, geom.PtF32(layoutCenter, top), textColor, layout, ui.AlignCenter) + renderer.DrawTexturePointOptions(ctx.Textures().ScaledByName(layoutTextures[i], scale), geom.PtF32(layoutLeft, layoutTop), ui.DrawOptions{Tint: layoutColor}) } } -func (s *settings) renderCustomLayout(ctx ui.Context) { +func (s *keyboardLayoutSettings) renderCustomLayout(ctx ui.Context) { runeOrQuestionMark := func(s string) rune { if len(s) == 0 { return '?' @@ -377,6 +424,323 @@ func (s *settings) renderCustomLayout(ctx ui.Context) { ctx.Textures().CreateTextureGo("layout-custom", generateCustomKeys(ctx.Resources(), customKeys), true) } +func (s *keyboardLayoutSettings) selectLayout() { + s.app.MenuInteraction() + switch s.ActiveLayout { + case 0: + s.SelectedLayout = 0 + s.app.Settings.Controls.Type = controlsTypeWASD + case 1: + s.SelectedLayout = 1 + s.app.Settings.Controls.Type = controlsTypeArrows + case 2: + s.SelectingCustom = 1 + } +} + +func (s *keyboardLayoutSettings) setActiveLayout(layout int) { + layout = (layout + 3) % 3 + change := layout != s.ActiveLayout + s.ActiveLayout = (layout + 3) % 3 + if change { + s.app.MenuInteraction() + } +} + +type settings struct { + ui.Proxy + + app *appContext + musicVolume *volumeControl + soundVolume *volumeControl + keyboard *keyboardLayoutSettings +} + +func newSettings(app *appContext, ctx ui.Context) *settings { + volumeControlTexture := generateTextureMapFromImage(ctx.Textures(), "volume-control", generateVolumeControlTexture) + + settings := &settings{app: app, + musicVolume: newVolumeControl(app, volumeControlTexture, "Music", app.Settings.Audio.MusicVolume, func(volume float64) { app.setMusicVolume(volume) }), + soundVolume: newVolumeControl(app, volumeControlTexture, "Sounds", app.Settings.Audio.SoundVolume, func(volume float64) { app.setSoundVolume(volume) }), + keyboard: newKeyboardLayoutSettings(app, ctx), + } + + settings.Content = ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) { + p.AddChild(Center(label("SETTINGS", "title"))) + p.AddChild(label("", "score")) + p.AddChild(Center(settings.musicVolume)) + p.AddChild(label("", "score")) + p.AddChild(Center(settings.soundVolume)) + p.AddChild(label("", "score")) + p.AddChild(settings.keyboard) + }) + + settings.musicVolume.Active = true + + return settings +} + +func (s *settings) currentActive() int { + switch { + case s.musicVolume.Active: + return 0 + case s.soundVolume.Active: + return 1 + case s.keyboard.Active: + return 2 + default: + return 0 + } +} + +func (s *settings) Handle(ctx ui.Context, e ui.Event) bool { + if s.keyboard.Active && s.keyboard.SelectingCustom > 0 { + return s.Proxy.Handle(ctx, e) + } + + switch e := e.(type) { + case *ui.KeyDownEvent: + switch e.Key { + case ui.KeyUp: + s.setActive(s.currentActive() - 1) + case ui.KeyDown: + s.setActive(s.currentActive() + 1) + } + } + + return s.Proxy.Handle(ctx, e) +} + +func (s *settings) Render(ctx ui.Context) { + s.Proxy.Render(ctx) + s.keyboard.PostRender(ctx) +} + +func (s *settings) setActive(active int) { + controls := [3]*bool{ + &s.musicVolume.Active, + &s.soundVolume.Active, + &s.keyboard.Active, + } + for active < 0 { + active += len(controls) + } + if active > 0 { + active = active % len(controls) + } + s.app.MenuInteraction() + + for i := range controls { + *controls[i] = i == active + } +} + func skewedKeyboardCoordinates(x, y float64) (float64, float64) { return x - keyboardKeySkew*y, y } + +var supportedCustomKeys = map[ui.Key]string{ + ui.KeyA: "A", + ui.KeyB: "B", + ui.KeyC: "C", + ui.KeyD: "D", + ui.KeyE: "E", + ui.KeyF: "F", + ui.KeyG: "G", + ui.KeyH: "H", + ui.KeyI: "I", + ui.KeyJ: "J", + ui.KeyK: "K", + ui.KeyL: "L", + ui.KeyM: "M", + ui.KeyN: "N", + ui.KeyO: "O", + ui.KeyP: "P", + ui.KeyQ: "Q", + ui.KeyR: "R", + ui.KeyS: "S", + ui.KeyT: "T", + ui.KeyU: "U", + ui.KeyV: "V", + ui.KeyW: "W", + ui.KeyX: "X", + ui.KeyY: "Y", + ui.KeyZ: "Z", +} + +type volumeControl struct { + ui.ControlBase + + app *appContext + texture *TextureMap + + Active bool + overLeftKnob bool + overBar int + overRightKnob bool + + Name string + Volume float64 + VolumeChanged func(float64) +} + +func newVolumeControl(app *appContext, texture *TextureMap, name string, volume float64, changed func(float64)) *volumeControl { + control := &volumeControl{app: app, texture: texture, overBar: -1, Name: name, Volume: volume, VolumeChanged: changed} + return control +} + +func (c *volumeControl) changeVolume(delta float64) { + c.setVolume(c.Volume + delta) +} + +func (c *volumeControl) setVolume(volume float64) { + volume = geom.Min(maxVolume, geom.Max(minVolume, volume)) + if c.Volume == volume { + return + } + c.Volume = volume + c.VolumeChanged(c.Volume) + c.app.MenuInteraction() +} + +func (c *volumeControl) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { + font := ctx.Fonts().Font("default") + return geom.PtF32(geom.NaN32(), 2.5*font.Height()) +} + +func (c *volumeControl) Handle(ctx ui.Context, e ui.Event) bool { + if c.ControlBase.Handle(ctx, e) { + return true + } + switch e := e.(type) { + case *ui.KeyDownEvent: + switch e.Key { + case ui.KeyLeft: + if c.Active { + c.changeVolume(-1) + } + case ui.KeyRight: + if c.Active { + c.changeVolume(1) + } + } + case *ui.MouseMoveEvent: + over := c.isOver(ctx, e.Pos()) + switch over { + case -1: + c.overLeftKnob = false + c.overBar = -1 + c.overRightKnob = false + case 0: + if !c.overLeftKnob { + c.app.MenuInteraction() + } + c.overLeftKnob = true + c.overBar = -1 + c.overRightKnob = false + case 11: + if !c.overRightKnob { + c.app.MenuInteraction() + } + c.overLeftKnob = false + c.overBar = -1 + c.overRightKnob = true + default: + if over-1 != c.overBar { + c.app.MenuInteraction() + } + c.overLeftKnob = false + c.overBar = over - 1 + c.overRightKnob = false + } + c.Active = c.IsOver() + case *ui.MouseButtonDownEvent: + if e.Button == ui.MouseButtonLeft { + over := c.isOver(ctx, e.Pos()) + switch over { + case -1: + case 0: + c.changeVolume(-1) + case 11: + c.changeVolume(1) + default: + c.setVolume(float64(over + minVolume)) + } + } + } + return false +} + +func (c *volumeControl) isOver(ctx ui.Context, p geom.PointF32) int { + bounds := c.Bounds() + font := ctx.Fonts().Font("default") + + top := bounds.Min.Y + 1.5*font.Height() + bottom := top + font.Height() + if p.Y < top || p.Y >= bottom { + return -1 + } + + scale := tins2021.FindScaleRound(volumeControlTextureHeight, font.Height()) + knobWidth := scale * volumeControlKnobWidth + barWidth := scale * volumeControlBarWidth + left := bounds.Center().X - 5*barWidth - knobWidth + if p.X < left { + return -1 + } + + widths := []float32{knobWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, knobWidth} + for i, w := range widths { + right := left + w + if p.X < right { + return i + } + left = right + } + return -1 +} + +func (c *volumeControl) Render(ctx ui.Context) { + font := ctx.Fonts().Font("default") + scale := tins2021.FindScaleRound(volumeControlTextureHeight, font.Height()) + + normalColor := ctx.Style().Palette.Text + activeColor := ctx.Style().Palette.Primary + + bounds := c.Bounds() + center := bounds.Center() + renderer := ctx.Renderer() + top := bounds.Min.Y + fontColor := normalColor + if c.Active { + fontColor = activeColor + } + renderer.TextAlign(font, geom.PtF32(center.X, top), fontColor, c.Name, ui.AlignCenter) + tint := func(active bool) color.Color { + if active { + return activeColor + } + return nil + } + + leftKnob, leftKnobRegion := c.texture.Scaled("leftKnob", scale) + leftKnobWidth := leftKnobRegion.Dx() + bar, barRegion := c.texture.Scaled("bar", scale) + barFilled, barFilledRegion := c.texture.Scaled("barFilled", scale) + barWidth := barRegion.Dx() + rightKnob, rightKnobRegion := c.texture.Scaled("rightKnob", scale) + left := center.X - 5*barWidth - leftKnobWidth + top += 1.5 * font.Height() + ctx.Renderer().DrawTexturePointOptions(leftKnob, geom.PtF32(left, top), ui.DrawOptions{Source: &leftKnobRegion, Tint: tint(c.overLeftKnob)}) + left += leftKnobWidth + for i := 0; i < 10; i++ { + volume := float64(i + minVolume + 1) + if volume <= c.Volume { + ctx.Renderer().DrawTexturePointOptions(barFilled, geom.PtF32(left, top), ui.DrawOptions{Source: &barFilledRegion, Tint: tint(i == c.overBar)}) + } else { + ctx.Renderer().DrawTexturePointOptions(bar, geom.PtF32(left, top), ui.DrawOptions{Source: &barRegion, Tint: tint(i == c.overBar)}) + } + left += barWidth + } + ctx.Renderer().DrawTexturePointOptions(rightKnob, geom.PtF32(left, top), ui.DrawOptions{Source: &rightKnobRegion, Tint: tint(c.overRightKnob)}) +} diff --git a/cmd/tins2021/texturemap.go b/cmd/tins2021/texturemap.go new file mode 100644 index 0000000..a3993fc --- /dev/null +++ b/cmd/tins2021/texturemap.go @@ -0,0 +1,42 @@ +package main + +import ( + "image" + + "opslag.de/schobers/geom" + "opslag.de/schobers/tins2021" + "opslag.de/schobers/zntg/ui" +) + +type TextureMap struct { + texture tins2021.NamedTexture + regions map[string]geom.RectangleF32 +} + +func newTextureMapFromImage(textures *ui.Textures, name string, im image.Image, regions map[string]geom.RectangleF) *TextureMap { + texture, err := tins2021.CreateNamedTextureImage(textures, name, im) + if err != nil { + panic(err) + } + regionMap := map[string]geom.RectangleF32{} + for name, region := range regions { + regionMap[name] = region.ToF32() + } + return &TextureMap{ + texture: texture, + regions: regionMap, + } +} + +func generateTextureMapFromImage(textures *ui.Textures, name string, generate func() (image.Image, map[string]geom.RectangleF)) *TextureMap { + im, regions := generate() + return newTextureMapFromImage(textures, name, im, regions) +} + +func (m *TextureMap) Texture(name string) (ui.Texture, geom.RectangleF32) { + return m.texture.Texture(), m.regions[name] +} + +func (m *TextureMap) Scaled(name string, scale float32) (ui.Texture, geom.RectangleF32) { + return m.texture.Scaled(scale), geom.RectangleF32{Min: m.regions[name].Min.Mul(scale), Max: m.regions[name].Max.Mul(scale)} +} diff --git a/settings.go b/settings.go index 7abcfca..13f6f82 100644 --- a/settings.go +++ b/settings.go @@ -8,7 +8,21 @@ import ( const settingsFileName = "settings.json" +type AudioSettings struct { + SoundVolume float64 + MusicVolume float64 +} + +type ControlsSettings struct { + Type string + MoveDownRight string + MoveDownLeft string + MoveUpLeft string + MoveUpRight string +} + type Settings struct { + Audio AudioSettings Controls ControlsSettings Window WindowSettings } @@ -25,14 +39,6 @@ func (s *Settings) Store() error { return SaveUserFileJSON(settingsFileName, s) } -type ControlsSettings struct { - Type string - MoveDownRight string - MoveDownLeft string - MoveUpLeft string - MoveUpRight string -} - type WindowSettings struct { Location *geom.Point Size *geom.Point