added implementation in GO lang
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
|||||||
/.vs
|
/.vs
|
||||||
/out
|
/out
|
||||||
|
/kidskeyboard
|
||||||
|
/kidskeyboard.exe
|
||||||
|
/KidsKeyboard.exe
|
||||||
|
|||||||
80
README.md
Normal file
80
README.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# KidsKeyboard
|
||||||
|
|
||||||
|
Jednoducha detska aplikacia v Go a Ebitengine. Po stlaceni klaves kresli efekty, zobrazuje obrazky a prehrava zvuky. Projekt je pripraveny tak, aby bezal aj bez externych assetov.
|
||||||
|
|
||||||
|
## Spustenie na Windows
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go build -o KidsKeyboard.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spustenie na Linuxe / Raspberry Pi
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build -o kidskeyboard
|
||||||
|
```
|
||||||
|
|
||||||
|
Na Raspberry Pi mozu byt potrebne systemove kniznice pre desktop, OpenGL/EGL, ALSA a X11/Wayland. Na Debian/Raspberry Pi OS typicky zacnite balickami ako:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install libasound2-dev libgl1-mesa-dev xorg-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fullscreen
|
||||||
|
|
||||||
|
Fullscreen zapnete prepinasom:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run . --fullscreen
|
||||||
|
```
|
||||||
|
|
||||||
|
Alebo cez prostredie:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
KIDSKEYBOARD_FULLSCREEN=1 go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
Okno je predvolene 1280x720. Rozmery mozete zmenit:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run . --width 1024 --height 768
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ovladenie
|
||||||
|
|
||||||
|
- `CTRL+F1` az `CTRL+F12` prepina rezimy.
|
||||||
|
- Predvoleny rezim po starte je `CTRL+F1`.
|
||||||
|
- `CTRL+SHIFT+ESC` ukonci aplikaciu.
|
||||||
|
- Samotny `ESC` aplikaciu neukoncuje.
|
||||||
|
|
||||||
|
## Rezimy
|
||||||
|
|
||||||
|
- `CTRL+F1` Keyboard mode: kreslena klavesnica, svietiace klavesy a generovane tony.
|
||||||
|
- `CTRL+F2` Geometry mode: tvary pre klavesy, `DELETE` vymaze vsetko, `+`/`-` meni velkost posledneho tvaru, sipky ho posuvaju.
|
||||||
|
- `CTRL+F3` Animal mode: nahodne zvieratko a zvuk. Podporovane assety su napriklad `assets/animals/dog.png` a `assets/animals/dog.wav`.
|
||||||
|
- `CTRL+F4` Calculator mode: jednoducha kalkulacka s pip/error zvukmi.
|
||||||
|
- `CTRL+F5` Find key mode: hlada sa zobrazene pismeno alebo cislo.
|
||||||
|
- `CTRL+F6` az `CTRL+F12`: zatial neimplementovane obrazovky.
|
||||||
|
|
||||||
|
## Assety
|
||||||
|
|
||||||
|
Volitelne subory:
|
||||||
|
|
||||||
|
- `assets/animals/dog.png`, `assets/animals/dog.wav`
|
||||||
|
- `assets/animals/cat.png`, `assets/animals/cat.wav`
|
||||||
|
- `assets/animals/cow.png`, `assets/animals/cow.wav`
|
||||||
|
- `assets/sounds/jingle1.wav` az `assets/sounds/jingle4.wav`
|
||||||
|
|
||||||
|
Ak obrazok alebo zvuk chyba, aplikacia pouzije textovy alebo programovo generovany fallback.
|
||||||
1
assets/animals/.gitkeep
Normal file
1
assets/animals/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
assets/fonts/.gitkeep
Normal file
1
assets/fonts/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
assets/sounds/.gitkeep
Normal file
1
assets/sounds/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
18
go.mod
Normal file
18
go.mod
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module kidskeyboard
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.9
|
||||||
|
golang.org/x/image v0.31.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
||||||
|
github.com/ebitengine/oto/v3 v3.4.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.9.0 // indirect
|
||||||
|
github.com/jezek/xgb v1.1.1 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
)
|
||||||
24
go.sum
Normal file
24
go.sum
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 h1:+kz5iTT3L7uU+VhlMfTb8hHcxLO3TlaELlX8wa4XjA0=
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1/go.mod h1:lKJoeixeJwnFmYsBny4vvCJGVFc3aYDalhuDsfZzWHI=
|
||||||
|
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/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
|
||||||
|
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
|
||||||
|
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||||
|
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v4 v4.1.0 h1:eE3qa5Do4qhowZVIHjsrX5pYyyPN6sAFWMsO7QREm3U=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v4 v4.1.0/go.mod h1:/PD+aLjAJ0F2UoQx6hkOfXqWN7BkroDUMr5W+IT1dpE=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.9 h1:JdDag6Ndj12iD4lxQGG8kbsrh7ssj4Sbzth6r929H/M=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.9/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
|
||||||
|
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||||
|
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||||
|
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
119
internal/app/app.go
Normal file
119
internal/app/app.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/assets"
|
||||||
|
kbaudio "kidskeyboard/internal/audio"
|
||||||
|
"kidskeyboard/internal/modes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
cfg Config
|
||||||
|
audio *kbaudio.Manager
|
||||||
|
assets *assets.Manager
|
||||||
|
rng *rand.Rand
|
||||||
|
modes []modes.Mode
|
||||||
|
current int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg Config) *Game {
|
||||||
|
audio := kbaudio.NewManager()
|
||||||
|
assets := assets.NewManager()
|
||||||
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
ctx := modes.Context{
|
||||||
|
Audio: audio,
|
||||||
|
Assets: assets,
|
||||||
|
RNG: rng,
|
||||||
|
}
|
||||||
|
|
||||||
|
game := &Game{
|
||||||
|
cfg: cfg,
|
||||||
|
audio: audio,
|
||||||
|
assets: assets,
|
||||||
|
rng: rng,
|
||||||
|
modes: []modes.Mode{
|
||||||
|
modes.NewKeyboardMode(ctx),
|
||||||
|
modes.NewGeometryMode(ctx),
|
||||||
|
modes.NewAnimalMode(ctx),
|
||||||
|
modes.NewCalculatorMode(ctx),
|
||||||
|
modes.NewFindKeyMode(ctx),
|
||||||
|
modes.NewNotImplementedMode("CTRL+F6"),
|
||||||
|
modes.NewNotImplementedMode("CTRL+F7"),
|
||||||
|
modes.NewNotImplementedMode("CTRL+F8"),
|
||||||
|
modes.NewNotImplementedMode("CTRL+F9"),
|
||||||
|
modes.NewNotImplementedMode("CTRL+F10"),
|
||||||
|
modes.NewNotImplementedMode("CTRL+F11"),
|
||||||
|
modes.NewNotImplementedMode("CTRL+F12"),
|
||||||
|
},
|
||||||
|
current: 0,
|
||||||
|
}
|
||||||
|
game.modes[game.current].OnEnter()
|
||||||
|
return game
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Update() error {
|
||||||
|
g.audio.Update()
|
||||||
|
|
||||||
|
if /*ctrlPressed() && */ shiftPressed() && inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||||
|
return ebiten.Termination
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctrlPressed() {
|
||||||
|
for i, key := range modeKeys() {
|
||||||
|
if inpututil.IsKeyJustPressed(key) {
|
||||||
|
g.switchMode(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.modes[g.current].HandleInput()
|
||||||
|
g.modes[g.current].Update()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
|
g.modes[g.current].Draw(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
|
if outsideWidth <= 0 || outsideHeight <= 0 {
|
||||||
|
return g.cfg.WindowWidth, g.cfg.WindowHeight
|
||||||
|
}
|
||||||
|
return outsideWidth, outsideHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) switchMode(next int) {
|
||||||
|
if next < 0 || next >= len(g.modes) || next == g.current {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.modes[g.current].OnLeave()
|
||||||
|
g.current = next
|
||||||
|
g.modes[g.current].OnEnter()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctrlPressed() bool {
|
||||||
|
return ebiten.IsKeyPressed(ebiten.KeyControlLeft) ||
|
||||||
|
ebiten.IsKeyPressed(ebiten.KeyControlRight) ||
|
||||||
|
ebiten.IsKeyPressed(ebiten.KeyControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shiftPressed() bool {
|
||||||
|
return ebiten.IsKeyPressed(ebiten.KeyShiftLeft) ||
|
||||||
|
ebiten.IsKeyPressed(ebiten.KeyShiftRight) ||
|
||||||
|
ebiten.IsKeyPressed(ebiten.KeyShift)
|
||||||
|
}
|
||||||
|
|
||||||
|
func modeKeys() []ebiten.Key {
|
||||||
|
return []ebiten.Key{
|
||||||
|
ebiten.KeyF1, ebiten.KeyF2, ebiten.KeyF3, ebiten.KeyF4,
|
||||||
|
ebiten.KeyF5, ebiten.KeyF6, ebiten.KeyF7, ebiten.KeyF8,
|
||||||
|
ebiten.KeyF9, ebiten.KeyF10, ebiten.KeyF11, ebiten.KeyF12,
|
||||||
|
}
|
||||||
|
}
|
||||||
43
internal/app/config.go
Normal file
43
internal/app/config.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WindowWidth int
|
||||||
|
WindowHeight int
|
||||||
|
Fullscreen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseConfig() Config {
|
||||||
|
cfg := Config{
|
||||||
|
WindowWidth: 1280,
|
||||||
|
WindowHeight: 720,
|
||||||
|
Fullscreen: envBool("KIDSKEYBOARD_FULLSCREEN"),
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.IntVar(&cfg.WindowWidth, "width", cfg.WindowWidth, "window width")
|
||||||
|
flag.IntVar(&cfg.WindowHeight, "height", cfg.WindowHeight, "window height")
|
||||||
|
flag.BoolVar(&cfg.Fullscreen, "fullscreen", cfg.Fullscreen, "start in fullscreen mode")
|
||||||
|
flag.Bool("windowed", false, "force windowed mode")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.Lookup("windowed") != nil {
|
||||||
|
if v := flag.Lookup("windowed").Value.String(); v == "true" {
|
||||||
|
cfg.Fullscreen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func envBool(name string) bool {
|
||||||
|
switch os.Getenv(name) {
|
||||||
|
case "1", "true", "TRUE", "yes", "YES", "on", "ON":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
49
internal/assets/manager.go
Normal file
49
internal/assets/manager.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
images map[string]*ebiten.Image
|
||||||
|
misses map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
images: map[string]*ebiten.Image{},
|
||||||
|
misses: map[string]bool{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Image(path string) (*ebiten.Image, bool) {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
if img, ok := m.images[path]; ok {
|
||||||
|
return img, true
|
||||||
|
}
|
||||||
|
if m.misses[path] {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
m.misses[path] = true
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
src, _, err := image.Decode(f)
|
||||||
|
if err != nil {
|
||||||
|
m.misses[path] = true
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
img := ebiten.NewImageFromImage(src)
|
||||||
|
m.images[path] = img
|
||||||
|
return img, true
|
||||||
|
}
|
||||||
130
internal/audio/manager.go
Normal file
130
internal/audio/manager.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ebiaudio "github.com/hajimehoshi/ebiten/v2/audio"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/audio/wav"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sampleRate = 44100
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
context *ebiaudio.Context
|
||||||
|
players []*ebiaudio.Player
|
||||||
|
current *ebiaudio.Player
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{context: ebiaudio.NewContext(sampleRate)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Update() {
|
||||||
|
alive := m.players[:0]
|
||||||
|
for _, p := range m.players {
|
||||||
|
if p.IsPlaying() {
|
||||||
|
alive = append(alive, p)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = p.Close()
|
||||||
|
}
|
||||||
|
m.players = alive
|
||||||
|
if m.current != nil && !m.current.IsPlaying() {
|
||||||
|
_ = m.current.Close()
|
||||||
|
m.current = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) PlayTone(freq float64, d time.Duration, volume float64) {
|
||||||
|
m.playPCM(generateTone(freq, d, volume), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) PlayToneInterrupt(freq float64, d time.Duration, volume float64) {
|
||||||
|
m.playPCM(generateTone(freq, d, volume), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) PlayBeep() {
|
||||||
|
m.PlayTone(880, 80*time.Millisecond, 0.22)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) PlayError() {
|
||||||
|
data := append(generateTone(160, 110*time.Millisecond, 0.28), generateTone(110, 150*time.Millisecond, 0.28)...)
|
||||||
|
m.playPCM(data, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) PlayWAV(path string, interrupt bool) bool {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
stream, err := wav.DecodeWithSampleRate(sampleRate, f)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(stream)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m.playPCM(data, interrupt)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) StopCurrent() {
|
||||||
|
if m.current == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.current.Pause()
|
||||||
|
_ = m.current.Close()
|
||||||
|
m.current = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) playPCM(data []byte, interrupt bool) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if interrupt {
|
||||||
|
m.StopCurrent()
|
||||||
|
}
|
||||||
|
player := m.context.NewPlayerFromBytes(data)
|
||||||
|
player.Play()
|
||||||
|
if interrupt {
|
||||||
|
m.current = player
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.players = append(m.players, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTone(freq float64, d time.Duration, volume float64) []byte {
|
||||||
|
samples := int(float64(sampleRate) * d.Seconds())
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, samples*4))
|
||||||
|
for i := 0; i < samples; i++ {
|
||||||
|
t := float64(i) / sampleRate
|
||||||
|
env := envelope(i, samples)
|
||||||
|
v := int16(math.Sin(2*math.Pi*freq*t) * volume * env * math.MaxInt16)
|
||||||
|
_ = binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
_ = binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func envelope(i, samples int) float64 {
|
||||||
|
if samples <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
attack := sampleRate / 200
|
||||||
|
release := sampleRate / 100
|
||||||
|
if i < attack {
|
||||||
|
return float64(i) / float64(attack)
|
||||||
|
}
|
||||||
|
if samples-i < release {
|
||||||
|
return float64(samples-i) / float64(release)
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
85
internal/modes/animal.go
Normal file
85
internal/modes/animal.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type animal struct {
|
||||||
|
name string
|
||||||
|
image string
|
||||||
|
sound string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnimalMode struct {
|
||||||
|
ctx Context
|
||||||
|
animals []animal
|
||||||
|
current *animal
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnimalMode(ctx Context) *AnimalMode {
|
||||||
|
return &AnimalMode{
|
||||||
|
ctx: ctx,
|
||||||
|
animals: []animal{
|
||||||
|
{"dog", "assets/animals/dog.png", "assets/animals/dog.wav"},
|
||||||
|
{"cat", "assets/animals/cat.png", "assets/animals/cat.wav"},
|
||||||
|
{"cow", "assets/animals/cow.png", "assets/animals/cow.wav"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AnimalMode) Name() string { return "CTRL+F3 Animal" }
|
||||||
|
func (m *AnimalMode) OnEnter() {}
|
||||||
|
func (m *AnimalMode) OnLeave() { m.ctx.Audio.StopCurrent() }
|
||||||
|
|
||||||
|
func (m *AnimalMode) HandleInput() {
|
||||||
|
for _, key := range justPressedKeys() {
|
||||||
|
if !childKey(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a := m.animals[m.ctx.RNG.Intn(len(m.animals))]
|
||||||
|
m.current = &a
|
||||||
|
if !m.ctx.Audio.PlayWAV(filepath.Clean(a.sound), true) {
|
||||||
|
m.ctx.Audio.PlayToneInterrupt(330+float64(m.ctx.RNG.Intn(280)), 260*time.Millisecond, 0.26)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AnimalMode) Update() {}
|
||||||
|
|
||||||
|
func (m *AnimalMode) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Fill(color.Black)
|
||||||
|
w, h := screen.Bounds().Dx(), screen.Bounds().Dy()
|
||||||
|
ui.Text(screen, "CTRL+F3", 20, 20, color.White, 2)
|
||||||
|
if m.current == nil {
|
||||||
|
ui.CenteredText(screen, "ANIMAL", w/2, h/2, color.White, 7)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if img, ok := m.ctx.Assets.Image(m.current.image); ok {
|
||||||
|
drawImageCentered(screen, img, w/2, h/2, float64(w)*0.65, float64(h)*0.72)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.CenteredText(screen, strings.ToUpper(m.current.name), w/2, h/2, color.White, 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawImageCentered(screen *ebiten.Image, img *ebiten.Image, cx, cy int, maxW, maxH float64) {
|
||||||
|
iw, ih := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
|
if iw <= 0 || ih <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scale := min(maxW/float64(iw), maxH/float64(ih))
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Scale(scale, scale)
|
||||||
|
op.GeoM.Translate(float64(cx)-float64(iw)*scale/2, float64(cy)-float64(ih)*scale/2)
|
||||||
|
screen.DrawImage(img, op)
|
||||||
|
}
|
||||||
235
internal/modes/calculator.go
Normal file
235
internal/modes/calculator.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CalculatorMode struct {
|
||||||
|
ctx Context
|
||||||
|
display string
|
||||||
|
acc float64
|
||||||
|
op string
|
||||||
|
newEntry bool
|
||||||
|
err bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCalculatorMode(ctx Context) *CalculatorMode {
|
||||||
|
return &CalculatorMode{ctx: ctx, display: "0", newEntry: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) Name() string { return "CTRL+F4 Calculator" }
|
||||||
|
func (m *CalculatorMode) OnEnter() {}
|
||||||
|
func (m *CalculatorMode) OnLeave() {}
|
||||||
|
func (m *CalculatorMode) Update() {}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) HandleInput() {
|
||||||
|
handled := false
|
||||||
|
valid := false
|
||||||
|
chars := ebiten.AppendInputChars(nil)
|
||||||
|
|
||||||
|
for _, r := range chars {
|
||||||
|
handled = true
|
||||||
|
if m.handleChar(r) {
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range justPressedKeys() {
|
||||||
|
switch {
|
||||||
|
case key == ebiten.KeyDelete:
|
||||||
|
handled = true
|
||||||
|
m.clear()
|
||||||
|
valid = true
|
||||||
|
case key == ebiten.KeyBackspace:
|
||||||
|
handled = true
|
||||||
|
m.backspace()
|
||||||
|
valid = true
|
||||||
|
case key == ebiten.KeyEnter || key == ebiten.KeyNumpadEnter:
|
||||||
|
handled = true
|
||||||
|
m.equals()
|
||||||
|
valid = true
|
||||||
|
case isNumpadOperatorKey(key) && len(chars) == 0:
|
||||||
|
handled = true
|
||||||
|
m.operator(operatorForKey(key))
|
||||||
|
valid = true
|
||||||
|
default:
|
||||||
|
if childKey(key) && len(chars) == 0 {
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if valid {
|
||||||
|
m.ctx.Audio.PlayBeep()
|
||||||
|
} else if handled {
|
||||||
|
m.ctx.Audio.PlayError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) handleChar(r rune) bool {
|
||||||
|
switch {
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
m.digit(string(r))
|
||||||
|
return true
|
||||||
|
case r == '+' || r == '-' || r == '*' || r == '/':
|
||||||
|
m.operator(string(r))
|
||||||
|
return true
|
||||||
|
case r == 'c' || r == 'C':
|
||||||
|
m.clear()
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Fill(color.Black)
|
||||||
|
w, h := screen.Bounds().Dx(), screen.Bounds().Dy()
|
||||||
|
ui.Text(screen, "CTRL+F4", 20, 20, color.White, 2)
|
||||||
|
|
||||||
|
panelW := float32(min(float64(w)*0.78, 620))
|
||||||
|
keySize := panelW / 4
|
||||||
|
startX := (float32(w) - panelW) / 2
|
||||||
|
startY := float32(h)/2 - keySize*1.9
|
||||||
|
displayH := keySize * 0.72
|
||||||
|
|
||||||
|
ui.Rect(screen, startX, startY-displayH-16, panelW, displayH, color.NRGBA{R: 20, G: 20, B: 20, A: 255})
|
||||||
|
ui.RectOutline(screen, startX, startY-displayH-16, panelW, displayH, 2, color.White)
|
||||||
|
scale := 4.0
|
||||||
|
if len(m.display) > 12 {
|
||||||
|
scale = 3
|
||||||
|
}
|
||||||
|
ui.CenteredText(screen, m.display, int(startX+panelW/2), int(startY-displayH/2-16), color.White, scale)
|
||||||
|
|
||||||
|
keys := [][]string{
|
||||||
|
{"7", "8", "9", "/"},
|
||||||
|
{"4", "5", "6", "*"},
|
||||||
|
{"1", "2", "3", "-"},
|
||||||
|
{"C", "0", "=", "+"},
|
||||||
|
}
|
||||||
|
for r, row := range keys {
|
||||||
|
for c, label := range row {
|
||||||
|
x := startX + float32(c)*keySize
|
||||||
|
y := startY + float32(r)*keySize
|
||||||
|
ui.Rect(screen, x+4, y+4, keySize-8, keySize-8, color.NRGBA{R: 28, G: 28, B: 28, A: 255})
|
||||||
|
ui.RectOutline(screen, x+4, y+4, keySize-8, keySize-8, 2, color.White)
|
||||||
|
ui.CenteredText(screen, label, int(x+keySize/2), int(y+keySize/2), color.White, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) digit(d string) {
|
||||||
|
if m.err || m.newEntry || m.display == "0" {
|
||||||
|
m.display = d
|
||||||
|
m.newEntry = false
|
||||||
|
m.err = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(m.display) < 16 {
|
||||||
|
m.display += d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) operator(op string) {
|
||||||
|
if m.err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m.op != "" && !m.newEntry {
|
||||||
|
m.equals()
|
||||||
|
}
|
||||||
|
m.acc = m.value()
|
||||||
|
m.op = op
|
||||||
|
m.newEntry = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) equals() {
|
||||||
|
if m.err || m.op == "" {
|
||||||
|
m.newEntry = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
right := m.value()
|
||||||
|
var result float64
|
||||||
|
switch m.op {
|
||||||
|
case "+":
|
||||||
|
result = m.acc + right
|
||||||
|
case "-":
|
||||||
|
result = m.acc - right
|
||||||
|
case "*":
|
||||||
|
result = m.acc * right
|
||||||
|
case "/":
|
||||||
|
if right == 0 {
|
||||||
|
m.display = "DIV 0"
|
||||||
|
m.err = true
|
||||||
|
m.op = ""
|
||||||
|
m.newEntry = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result = m.acc / right
|
||||||
|
}
|
||||||
|
m.display = formatNumber(result)
|
||||||
|
m.acc = result
|
||||||
|
m.op = ""
|
||||||
|
m.newEntry = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) backspace() {
|
||||||
|
if m.err || m.newEntry || len(m.display) <= 1 {
|
||||||
|
m.display = "0"
|
||||||
|
m.err = false
|
||||||
|
m.newEntry = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.display = m.display[:len(m.display)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) clear() {
|
||||||
|
m.display = "0"
|
||||||
|
m.acc = 0
|
||||||
|
m.op = ""
|
||||||
|
m.err = false
|
||||||
|
m.newEntry = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CalculatorMode) value() float64 {
|
||||||
|
v, _ := strconv.ParseFloat(m.display, 64)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatNumber(v float64) string {
|
||||||
|
if math.Abs(v-math.Round(v)) < 0.0000001 {
|
||||||
|
return fmt.Sprintf("%.0f", v)
|
||||||
|
}
|
||||||
|
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.6f", v), "0"), ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumpadOperatorKey(key ebiten.Key) bool {
|
||||||
|
switch key {
|
||||||
|
case ebiten.KeyNumpadAdd, ebiten.KeyNumpadSubtract, ebiten.KeyNumpadMultiply, ebiten.KeyNumpadDivide:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func operatorForKey(key ebiten.Key) string {
|
||||||
|
switch key {
|
||||||
|
case ebiten.KeyNumpadAdd:
|
||||||
|
return "+"
|
||||||
|
case ebiten.KeyNumpadSubtract:
|
||||||
|
return "-"
|
||||||
|
case ebiten.KeyNumpadMultiply:
|
||||||
|
return "*"
|
||||||
|
case ebiten.KeyNumpadDivide:
|
||||||
|
return "/"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
100
internal/modes/findkey.go
Normal file
100
internal/modes/findkey.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FindKeyMode struct {
|
||||||
|
ctx Context
|
||||||
|
target rune
|
||||||
|
successTicks int
|
||||||
|
errorTicks int
|
||||||
|
lastJingle int
|
||||||
|
}
|
||||||
|
|
||||||
|
const findChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
func NewFindKeyMode(ctx Context) *FindKeyMode {
|
||||||
|
m := &FindKeyMode{ctx: ctx, lastJingle: -1}
|
||||||
|
m.pickNext()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FindKeyMode) Name() string { return "CTRL+F5 Find Key" }
|
||||||
|
func (m *FindKeyMode) OnEnter() {}
|
||||||
|
func (m *FindKeyMode) OnLeave() {}
|
||||||
|
|
||||||
|
func (m *FindKeyMode) HandleInput() {
|
||||||
|
if m.successTicks > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, key := range justPressedKeys() {
|
||||||
|
if !childKey(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r, ok := keyRune(key)
|
||||||
|
if ok && r == m.target {
|
||||||
|
m.successTicks = 180
|
||||||
|
m.errorTicks = 0
|
||||||
|
m.playJingle()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.errorTicks = 18
|
||||||
|
m.ctx.Audio.PlayError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FindKeyMode) Update() {
|
||||||
|
if m.errorTicks > 0 {
|
||||||
|
m.errorTicks--
|
||||||
|
}
|
||||||
|
if m.successTicks > 0 {
|
||||||
|
m.successTicks--
|
||||||
|
if m.successTicks == 0 {
|
||||||
|
m.pickNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FindKeyMode) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Fill(color.Black)
|
||||||
|
w, h := screen.Bounds().Dx(), screen.Bounds().Dy()
|
||||||
|
ui.Text(screen, "CTRL+F5", 20, 20, color.White, 2)
|
||||||
|
|
||||||
|
var clr color.Color = color.White
|
||||||
|
if m.errorTicks > 0 {
|
||||||
|
clr = color.NRGBA{R: 255, A: 255}
|
||||||
|
}
|
||||||
|
if m.successTicks > 0 {
|
||||||
|
clr = color.NRGBA{G: 255, A: 255}
|
||||||
|
}
|
||||||
|
ui.CenteredText(screen, string(m.target), w/2, h/2, clr, 18)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FindKeyMode) pickNext() {
|
||||||
|
m.target = rune(findChars[m.ctx.RNG.Intn(len(findChars))])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FindKeyMode) playJingle() {
|
||||||
|
const count = 4
|
||||||
|
next := m.ctx.RNG.Intn(count)
|
||||||
|
if count > 1 {
|
||||||
|
for next == m.lastJingle {
|
||||||
|
next = m.ctx.RNG.Intn(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.lastJingle = next
|
||||||
|
path := filepath.Join("assets", "sounds", "jingle"+string(rune('1'+next))+".wav")
|
||||||
|
if m.ctx.Audio.PlayWAV(path, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
freq := 520 + float64(next)*120
|
||||||
|
m.ctx.Audio.PlayToneInterrupt(freq, 180*time.Millisecond, 0.28)
|
||||||
|
}
|
||||||
180
internal/modes/geometry.go
Normal file
180
internal/modes/geometry.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type shapeKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
shapeCircle shapeKind = iota
|
||||||
|
shapeRect
|
||||||
|
shapeTriangle
|
||||||
|
shapeLine
|
||||||
|
shapeStar
|
||||||
|
)
|
||||||
|
|
||||||
|
type shape struct {
|
||||||
|
kind shapeKind
|
||||||
|
x float32
|
||||||
|
y float32
|
||||||
|
w float32
|
||||||
|
h float32
|
||||||
|
size float32
|
||||||
|
color color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeometryMode struct {
|
||||||
|
ctx Context
|
||||||
|
shapes map[ebiten.Key]*shape
|
||||||
|
order []ebiten.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeometryMode(ctx Context) *GeometryMode {
|
||||||
|
return &GeometryMode{ctx: ctx, shapes: map[ebiten.Key]*shape{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GeometryMode) Name() string { return "CTRL+F2 Geometry" }
|
||||||
|
func (m *GeometryMode) OnEnter() {}
|
||||||
|
func (m *GeometryMode) OnLeave() {}
|
||||||
|
|
||||||
|
func (m *GeometryMode) HandleInput() {
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyDelete) {
|
||||||
|
m.shapes = map[ebiten.Key]*shape{}
|
||||||
|
m.order = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
|
||||||
|
m.moveLast(-20, 0)
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
|
||||||
|
m.moveLast(20, 0)
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
|
||||||
|
m.moveLast(0, -20)
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
|
||||||
|
m.moveLast(0, 20)
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyEqual) || inpututil.IsKeyJustPressed(ebiten.KeyNumpadAdd) {
|
||||||
|
m.scaleLast(1.12)
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyMinus) || inpututil.IsKeyJustPressed(ebiten.KeyNumpadSubtract) {
|
||||||
|
m.scaleLast(0.88)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range justPressedKeys() {
|
||||||
|
if !childKey(key) || geometryControlKey(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := m.shapes[key]; ok {
|
||||||
|
delete(m.shapes, key)
|
||||||
|
m.removeOrder(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.shapes[key] = m.randomShape()
|
||||||
|
m.order = append(m.order, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GeometryMode) Update() {}
|
||||||
|
|
||||||
|
func (m *GeometryMode) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Fill(color.Black)
|
||||||
|
for _, key := range m.order {
|
||||||
|
if s := m.shapes[key]; s != nil {
|
||||||
|
drawShape(screen, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.Text(screen, "CTRL+F2", 20, 20, color.White, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GeometryMode) randomShape() *shape {
|
||||||
|
w, h := 1280, 720
|
||||||
|
x := float32(80 + m.ctx.RNG.Intn(max(1, w-160)))
|
||||||
|
y := float32(80 + m.ctx.RNG.Intn(max(1, h-160)))
|
||||||
|
size := float32(35 + m.ctx.RNG.Intn(100))
|
||||||
|
return &shape{
|
||||||
|
kind: shapeKind(m.ctx.RNG.Intn(5)),
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
w: size + float32(m.ctx.RNG.Intn(90)),
|
||||||
|
h: size + float32(m.ctx.RNG.Intn(90)),
|
||||||
|
size: size,
|
||||||
|
color: color.NRGBA{
|
||||||
|
R: uint8(80 + m.ctx.RNG.Intn(176)),
|
||||||
|
G: uint8(80 + m.ctx.RNG.Intn(176)),
|
||||||
|
B: uint8(80 + m.ctx.RNG.Intn(176)),
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawShape(screen *ebiten.Image, s *shape) {
|
||||||
|
switch s.kind {
|
||||||
|
case shapeCircle:
|
||||||
|
ui.Circle(screen, s.x, s.y, s.size, s.color)
|
||||||
|
case shapeRect:
|
||||||
|
ui.Rect(screen, s.x-s.w/2, s.y-s.h/2, s.w, s.h, s.color)
|
||||||
|
case shapeTriangle:
|
||||||
|
ui.Triangle(screen, s.x, s.y-s.h/2, s.x-s.w/2, s.y+s.h/2, s.x+s.w/2, s.y+s.h/2, s.color)
|
||||||
|
case shapeLine:
|
||||||
|
ui.Line(screen, s.x-s.w/2, s.y-s.h/2, s.x+s.w/2, s.y+s.h/2, float32(math.Max(4, float64(s.size/8))), s.color)
|
||||||
|
case shapeStar:
|
||||||
|
ui.Star(screen, s.x, s.y, s.size, s.size*0.45, 5, s.color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GeometryMode) moveLast(dx, dy float32) {
|
||||||
|
last := m.last()
|
||||||
|
if last == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
last.x += dx
|
||||||
|
last.y += dy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GeometryMode) scaleLast(factor float32) {
|
||||||
|
last := m.last()
|
||||||
|
if last == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
last.w *= factor
|
||||||
|
last.h *= factor
|
||||||
|
last.size *= factor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GeometryMode) last() *shape {
|
||||||
|
for i := len(m.order) - 1; i >= 0; i-- {
|
||||||
|
if s := m.shapes[m.order[i]]; s != nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GeometryMode) removeOrder(key ebiten.Key) {
|
||||||
|
next := m.order[:0]
|
||||||
|
for _, k := range m.order {
|
||||||
|
if k != key {
|
||||||
|
next = append(next, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.order = next
|
||||||
|
}
|
||||||
|
|
||||||
|
func geometryControlKey(key ebiten.Key) bool {
|
||||||
|
switch key {
|
||||||
|
case ebiten.KeyDelete, ebiten.KeyArrowLeft, ebiten.KeyArrowRight, ebiten.KeyArrowUp, ebiten.KeyArrowDown,
|
||||||
|
ebiten.KeyEqual, ebiten.KeyMinus, ebiten.KeyNumpadAdd, ebiten.KeyNumpadSubtract:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
83
internal/modes/input.go
Normal file
83
internal/modes/input.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func justPressedKeys() []ebiten.Key {
|
||||||
|
var keys []ebiten.Key
|
||||||
|
keys = inpututil.AppendJustPressedKeys(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func childKey(key ebiten.Key) bool {
|
||||||
|
switch key {
|
||||||
|
case ebiten.KeyControl, ebiten.KeyControlLeft, ebiten.KeyControlRight,
|
||||||
|
ebiten.KeyShift, ebiten.KeyShiftLeft, ebiten.KeyShiftRight,
|
||||||
|
ebiten.KeyAlt, ebiten.KeyAltLeft, ebiten.KeyAltRight,
|
||||||
|
ebiten.KeyMeta, ebiten.KeyEscape,
|
||||||
|
ebiten.KeyF1, ebiten.KeyF2, ebiten.KeyF3, ebiten.KeyF4,
|
||||||
|
ebiten.KeyF5, ebiten.KeyF6, ebiten.KeyF7, ebiten.KeyF8,
|
||||||
|
ebiten.KeyF9, ebiten.KeyF10, ebiten.KeyF11, ebiten.KeyF12:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyLabel(key ebiten.Key) string {
|
||||||
|
if r, ok := keyRune(key); ok {
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case ebiten.KeySpace:
|
||||||
|
return "SPACE"
|
||||||
|
case ebiten.KeyEnter, ebiten.KeyNumpadEnter:
|
||||||
|
return "ENTER"
|
||||||
|
case ebiten.KeyBackspace:
|
||||||
|
return "BKSP"
|
||||||
|
case ebiten.KeyDelete:
|
||||||
|
return "DEL"
|
||||||
|
case ebiten.KeyTab:
|
||||||
|
return "TAB"
|
||||||
|
case ebiten.KeyArrowLeft:
|
||||||
|
return "LEFT"
|
||||||
|
case ebiten.KeyArrowRight:
|
||||||
|
return "RIGHT"
|
||||||
|
case ebiten.KeyArrowUp:
|
||||||
|
return "UP"
|
||||||
|
case ebiten.KeyArrowDown:
|
||||||
|
return "DOWN"
|
||||||
|
default:
|
||||||
|
return key.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyRune(key ebiten.Key) (rune, bool) {
|
||||||
|
if key >= ebiten.KeyA && key <= ebiten.KeyZ {
|
||||||
|
return rune('A' + int(key-ebiten.KeyA)), true
|
||||||
|
}
|
||||||
|
if key >= ebiten.KeyDigit0 && key <= ebiten.KeyDigit9 {
|
||||||
|
return rune('0' + int(key-ebiten.KeyDigit0)), true
|
||||||
|
}
|
||||||
|
if key >= ebiten.KeyNumpad0 && key <= ebiten.KeyNumpad9 {
|
||||||
|
return rune('0' + int(key-ebiten.KeyNumpad0)), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func digitValue(key ebiten.Key) (string, bool) {
|
||||||
|
r, ok := keyRune(key)
|
||||||
|
if !ok || r < '0' || r > '9' {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return string(r), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDigit(s string) int {
|
||||||
|
v, _ := strconv.Atoi(s)
|
||||||
|
return v
|
||||||
|
}
|
||||||
96
internal/modes/keyboard.go
Normal file
96
internal/modes/keyboard.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyboardMode struct {
|
||||||
|
ctx Context
|
||||||
|
highlight map[ebiten.Key]float64
|
||||||
|
layout [][]ebiten.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKeyboardMode(ctx Context) *KeyboardMode {
|
||||||
|
return &KeyboardMode{
|
||||||
|
ctx: ctx,
|
||||||
|
highlight: map[ebiten.Key]float64{},
|
||||||
|
layout: [][]ebiten.Key{
|
||||||
|
{ebiten.KeyDigit1, ebiten.KeyDigit2, ebiten.KeyDigit3, ebiten.KeyDigit4, ebiten.KeyDigit5, ebiten.KeyDigit6, ebiten.KeyDigit7, ebiten.KeyDigit8, ebiten.KeyDigit9, ebiten.KeyDigit0},
|
||||||
|
{ebiten.KeyQ, ebiten.KeyW, ebiten.KeyE, ebiten.KeyR, ebiten.KeyT, ebiten.KeyY, ebiten.KeyU, ebiten.KeyI, ebiten.KeyO, ebiten.KeyP},
|
||||||
|
{ebiten.KeyA, ebiten.KeyS, ebiten.KeyD, ebiten.KeyF, ebiten.KeyG, ebiten.KeyH, ebiten.KeyJ, ebiten.KeyK, ebiten.KeyL},
|
||||||
|
{ebiten.KeyZ, ebiten.KeyX, ebiten.KeyC, ebiten.KeyV, ebiten.KeyB, ebiten.KeyN, ebiten.KeyM},
|
||||||
|
{ebiten.KeySpace, ebiten.KeyEnter, ebiten.KeyBackspace},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KeyboardMode) Name() string { return "CTRL+F1 Keyboard" }
|
||||||
|
func (m *KeyboardMode) OnEnter() {}
|
||||||
|
func (m *KeyboardMode) OnLeave() {}
|
||||||
|
|
||||||
|
func (m *KeyboardMode) HandleInput() {
|
||||||
|
for _, key := range justPressedKeys() {
|
||||||
|
if !childKey(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.highlight[key] = 1
|
||||||
|
freq := 220 + float64(int(key)%48)*18
|
||||||
|
m.ctx.Audio.PlayTone(freq, 120*time.Millisecond, 0.22)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KeyboardMode) Update() {
|
||||||
|
for key, v := range m.highlight {
|
||||||
|
v -= 0.035
|
||||||
|
if v <= 0 {
|
||||||
|
delete(m.highlight, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.highlight[key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KeyboardMode) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Fill(color.Black)
|
||||||
|
w, h := screen.Bounds().Dx(), screen.Bounds().Dy()
|
||||||
|
keyW := float32(w) / 13
|
||||||
|
keyH := float32(h) / 10
|
||||||
|
gap := float32(8)
|
||||||
|
startY := float32(h)/2 - keyH*2.7
|
||||||
|
|
||||||
|
for rowIndex, row := range m.layout {
|
||||||
|
rowWidth := float32(len(row))*(keyW+gap) - gap
|
||||||
|
if rowIndex == 4 {
|
||||||
|
rowWidth = keyW*7 + gap*2
|
||||||
|
}
|
||||||
|
x := (float32(w) - rowWidth) / 2
|
||||||
|
y := startY + float32(rowIndex)*(keyH+gap)
|
||||||
|
for _, key := range row {
|
||||||
|
drawW := keyW
|
||||||
|
if key == ebiten.KeySpace {
|
||||||
|
drawW = keyW * 3
|
||||||
|
}
|
||||||
|
if key == ebiten.KeyEnter || key == ebiten.KeyBackspace {
|
||||||
|
drawW = keyW * 2
|
||||||
|
}
|
||||||
|
m.drawKey(screen, key, x, y, drawW, keyH)
|
||||||
|
x += drawW + gap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.CenteredText(screen, "CTRL+F1", w/2, 48, color.White, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KeyboardMode) drawKey(screen *ebiten.Image, key ebiten.Key, x, y, w, h float32) {
|
||||||
|
base := color.NRGBA{R: 18, G: 18, B: 18, A: 255}
|
||||||
|
if v := m.highlight[key]; v > 0 {
|
||||||
|
base = color.NRGBA{R: uint8(60 + 180*v), G: uint8(80 + 160*v), B: 255, A: 255}
|
||||||
|
}
|
||||||
|
ui.Rect(screen, x, y, w, h, base)
|
||||||
|
ui.RectOutline(screen, x, y, w, h, 2, color.White)
|
||||||
|
ui.CenteredText(screen, keyLabel(key), int(x+w/2), int(y+h/2), color.White, 2)
|
||||||
|
}
|
||||||
25
internal/modes/mode.go
Normal file
25
internal/modes/mode.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/assets"
|
||||||
|
kbaudio "kidskeyboard/internal/audio"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Audio *kbaudio.Manager
|
||||||
|
Assets *assets.Manager
|
||||||
|
RNG *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mode interface {
|
||||||
|
Name() string
|
||||||
|
OnEnter()
|
||||||
|
OnLeave()
|
||||||
|
HandleInput()
|
||||||
|
Update()
|
||||||
|
Draw(screen *ebiten.Image)
|
||||||
|
}
|
||||||
34
internal/modes/notimplemented.go
Normal file
34
internal/modes/notimplemented.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotImplementedMode struct {
|
||||||
|
label string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotImplementedMode(label string) *NotImplementedMode {
|
||||||
|
return &NotImplementedMode{label: label}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NotImplementedMode) Name() string { return m.label }
|
||||||
|
func (m *NotImplementedMode) OnEnter() {}
|
||||||
|
func (m *NotImplementedMode) OnLeave() {}
|
||||||
|
func (m *NotImplementedMode) HandleInput() {}
|
||||||
|
func (m *NotImplementedMode) Update() {}
|
||||||
|
func (m *NotImplementedMode) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Fill(color.Black)
|
||||||
|
w, h := screen.Bounds().Dx(), screen.Bounds().Dy()
|
||||||
|
boxW := float32(w) * 0.62
|
||||||
|
boxH := float32(h) * 0.28
|
||||||
|
x := (float32(w) - boxW) / 2
|
||||||
|
y := (float32(h) - boxH) / 2
|
||||||
|
ui.RectOutline(screen, x, y, boxW, boxH, 5, color.NRGBA{R: 255, G: 220, A: 255})
|
||||||
|
ui.CenteredText(screen, "Este nebolo implementovane", w/2, h/2-24, color.NRGBA{R: 255, A: 255}, 3)
|
||||||
|
ui.CenteredText(screen, m.label, w/2, h/2+42, color.White, 3)
|
||||||
|
}
|
||||||
117
internal/ui/draw.go
Normal file
117
internal/ui/draw.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
"golang.org/x/image/font/basicfont"
|
||||||
|
)
|
||||||
|
|
||||||
|
var whitePixel = func() *ebiten.Image {
|
||||||
|
img := ebiten.NewImage(1, 1)
|
||||||
|
img.Fill(color.White)
|
||||||
|
return img
|
||||||
|
}()
|
||||||
|
|
||||||
|
func Rect(screen *ebiten.Image, x, y, w, h float32, clr color.Color) {
|
||||||
|
vector.FillRect(screen, x, y, w, h, clr, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RectOutline(screen *ebiten.Image, x, y, w, h, stroke float32, clr color.Color) {
|
||||||
|
vector.StrokeRect(screen, x, y, w, h, stroke, clr, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Line(screen *ebiten.Image, x1, y1, x2, y2, width float32, clr color.Color) {
|
||||||
|
vector.StrokeLine(screen, x1, y1, x2, y2, width, clr, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Circle(screen *ebiten.Image, x, y, r float32, clr color.Color) {
|
||||||
|
vector.FillCircle(screen, x, y, r, clr, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Triangle(screen *ebiten.Image, x1, y1, x2, y2, x3, y3 float32, clr color.Color) {
|
||||||
|
Polygon(screen, []Point{{x1, y1}, {x2, y2}, {x3, y3}}, clr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Star(screen *ebiten.Image, x, y, outer, inner float32, points int, clr color.Color) {
|
||||||
|
if points < 3 {
|
||||||
|
points = 5
|
||||||
|
}
|
||||||
|
poly := make([]Point, 0, points*2)
|
||||||
|
for i := 0; i < points*2; i++ {
|
||||||
|
r := outer
|
||||||
|
if i%2 == 1 {
|
||||||
|
r = inner
|
||||||
|
}
|
||||||
|
a := -math.Pi/2 + float64(i)*math.Pi/float64(points)
|
||||||
|
poly = append(poly, Point{
|
||||||
|
X: x + float32(math.Cos(a))*r,
|
||||||
|
Y: y + float32(math.Sin(a))*r,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for i := 0; i < len(poly); i++ {
|
||||||
|
next := (i + 1) % len(poly)
|
||||||
|
Triangle(screen, x, y, poly[i].X, poly[i].Y, poly[next].X, poly[next].Y, clr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
X float32
|
||||||
|
Y float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func Polygon(screen *ebiten.Image, points []Point, clr color.Color) {
|
||||||
|
if len(points) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, g, b, a := clr.RGBA()
|
||||||
|
vertices := make([]ebiten.Vertex, len(points))
|
||||||
|
for i, p := range points {
|
||||||
|
vertices[i] = ebiten.Vertex{
|
||||||
|
DstX: p.X,
|
||||||
|
DstY: p.Y,
|
||||||
|
SrcX: 0,
|
||||||
|
SrcY: 0,
|
||||||
|
ColorR: float32(r) / 0xffff,
|
||||||
|
ColorG: float32(g) / 0xffff,
|
||||||
|
ColorB: float32(b) / 0xffff,
|
||||||
|
ColorA: float32(a) / 0xffff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indices := make([]uint16, 0, (len(points)-2)*3)
|
||||||
|
for i := 1; i < len(points)-1; i++ {
|
||||||
|
indices = append(indices, 0, uint16(i), uint16(i+1))
|
||||||
|
}
|
||||||
|
screen.DrawTriangles(vertices, indices, whitePixel, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Text(screen *ebiten.Image, s string, x, y int, clr color.Color, scale float64) {
|
||||||
|
drawText(screen, s, x, y, clr, scale, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CenteredText(screen *ebiten.Image, s string, cx, cy int, clr color.Color, scale float64) {
|
||||||
|
drawText(screen, s, cx, cy, clr, scale, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawText(screen *ebiten.Image, s string, x, y int, clr color.Color, scale float64, centered bool) {
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
w := max(1, len(s)*7+4)
|
||||||
|
h := 17
|
||||||
|
img := ebiten.NewImage(w, h)
|
||||||
|
text.Draw(img, s, basicfont.Face7x13, 2, 13, clr)
|
||||||
|
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Scale(scale, scale)
|
||||||
|
tx, ty := float64(x), float64(y)
|
||||||
|
if centered {
|
||||||
|
tx -= float64(w) * scale / 2
|
||||||
|
ty -= float64(h) * scale / 2
|
||||||
|
}
|
||||||
|
op.GeoM.Translate(tx, ty)
|
||||||
|
screen.DrawImage(img, op)
|
||||||
|
}
|
||||||
23
main.go
Normal file
23
main.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"kidskeyboard/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := app.ParseConfig()
|
||||||
|
game := app.New(cfg)
|
||||||
|
|
||||||
|
ebiten.SetWindowTitle("KidsKeyboard")
|
||||||
|
ebiten.SetWindowSize(cfg.WindowWidth, cfg.WindowHeight)
|
||||||
|
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
|
||||||
|
ebiten.SetFullscreen(cfg.Fullscreen)
|
||||||
|
|
||||||
|
if err := ebiten.RunGame(game); err != nil && err != ebiten.Termination {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user