added implementation in GO lang

This commit is contained in:
2026-05-22 13:00:28 +02:00
parent 67ee6c268c
commit ddac1d02cd
21 changed files with 1447 additions and 0 deletions

85
internal/modes/animal.go Normal file
View 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)
}

View 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
View 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
View 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
View 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
}

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

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