From d5d9badb058fddcb69440a99547528995a1b6055 Mon Sep 17 00:00:00 2001 From: roodletoof <68161791+roodletoof@users.noreply.github.com> Date: Sat, 14 Sep 2024 15:16:36 +0200 Subject: [PATCH] initial --- .r.sh | 1 + go.mod | 14 ++++++ go.sum | 16 +++++++ keymap.go | 35 ++++++++++++++ main.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++ strokes.go | 95 +++++++++++++++++++++++++++++++++++++ 6 files changed, 295 insertions(+) create mode 100644 .r.sh create mode 100644 go.mod create mode 100644 go.sum create mode 100644 keymap.go create mode 100644 main.go create mode 100644 strokes.go diff --git a/.r.sh b/.r.sh new file mode 100644 index 0000000..329a9e8 --- /dev/null +++ b/.r.sh @@ -0,0 +1 @@ +go run . temp.png diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..194671d --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/roodletoof/whiteboard + +go 1.23.1 + +require github.com/hajimehoshi/ebiten/v2 v2.7.9 + +require ( + github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect + github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/purego v0.7.0 // indirect + github.com/jezek/xgb v1.1.1 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4b24e03 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU= +github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M= +github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= +github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= +github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc= +github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/hajimehoshi/ebiten/v2 v2.7.9 h1:DYH/usAa9dMHcGkBIIEApJsVqDekrJBxYHmsBuly8Iw= +github.com/hajimehoshi/ebiten/v2 v2.7.9/go.mod h1:Ulbq5xDmdx47P24EJ+Mb31Zps7vQq+guieG9mghQUaA= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/keymap.go b/keymap.go new file mode 100644 index 0000000..4968cd6 --- /dev/null +++ b/keymap.go @@ -0,0 +1,35 @@ +package main + +import "github.com/hajimehoshi/ebiten/v2" + + +type keymap []ebiten.Key +var resize = keymap{ + ebiten.KeyD, + ebiten.KeyK, +} +var drawBlack = keymap{ + ebiten.KeyF, + ebiten.KeyJ, +} +var drawWhite = keymap{ + ebiten.KeyS, + ebiten.KeyL, +} +var undo = keymap{ + ebiten.KeyZ, + ebiten.KeyU, +} +var redo = keymap{ + ebiten.KeyX, + ebiten.KeyR, +} + +func (k keymap) check(checker func(ebiten.Key) bool) bool { + for _, key := range k { + if checker(key) { + return true + } + } + return false +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..5889ef5 --- /dev/null +++ b/main.go @@ -0,0 +1,134 @@ +package main + +import ( + "image/color" + "log" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +const width = 512 +const height = 512 + +type appOption func(a *app) + +type app struct { + strokes []stroke + width float32 + canvas *ebiten.Image + sizeAnchorPos Vec2 + historyIndex uintptr +} + +func (a *app) Draw(screen *ebiten.Image) { + screen.Fill(color.White) + + opts := ebiten.DrawImageOptions{} + screen.DrawImage(a.canvas, &opts) + + cursor := cursorPosition() + vector.StrokeCircle( + screen, + cursor.x, + cursor.y, + a.width/2, + 3, + color.White, + true, + ) + vector.StrokeCircle( + screen, + cursor.x, + cursor.y, + a.width/2, + 1, + color.Black, + true, + ) +} + +func (a *app) Layout(_, _ int) (screenWidth int, screenHeight int) { + return width, height +} + +func (a *app) Update() error { + + check_any_drawing_input := func(checker func(ebiten.Key) bool) bool { + return drawWhite.check(checker) || drawBlack.check(checker) + } + + is_drawing := check_any_drawing_input(ebiten.IsKeyPressed) + just_started_drawing := check_any_drawing_input(inpututil.IsKeyJustPressed) + + var clr color.Color + if drawBlack.check(ebiten.IsKeyPressed) { + clr = color.Black + } else if drawWhite.check(ebiten.IsKeyPressed) { + clr = color.White + } + + if just_started_drawing { + if clr == nil { + log.Fatal("drawing color in update is nil") // should never happen + } + a.strokes = append(a.strokes, stroke{ + points: []Vec2{}, + width: a.width, + color: clr, + }) + } + // TODO current history pointer is not updated as it should + // TODO tests + + if len(a.strokes) > 0 { + currentStroke := &a.strokes[len(a.strokes)-1] + if is_drawing { + currentStroke.appendIfMoved(cursorPosition()) + currentStroke.drawLast(a.canvas) + } + } + + if resize.check(inpututil.IsKeyJustPressed) { + a.sizeAnchorPos = cursorPosition() + } + + if resize.check(ebiten.IsKeyPressed) { + prevPos := a.sizeAnchorPos + currPos := cursorPosition() + reltivePos := Vec2{ + prevPos.x - currPos.x, + prevPos.y - currPos.y, + } + length := float32(math.Sqrt( + float64(reltivePos.x) * float64(reltivePos.x) + float64(reltivePos.y) * float64(reltivePos.y), + )) + a.width = length * 2.0 + } + + return nil +} + +func newApp( opts ...appOption ) app { + a := app{} + for _, opt := range opts { + opt(&a) + } + return a +} + +func main() { + + ebiten.SetWindowTitle("whiteboard") + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + ebiten.CursorPosition() + whiteboardApp := newApp( func(a *app) { + a.width = 10 + a.canvas = ebiten.NewImage(width, height) + a.canvas.Fill(color.White) + }) + + ebiten.RunGame(&whiteboardApp) +} diff --git a/strokes.go b/strokes.go new file mode 100644 index 0000000..357008d --- /dev/null +++ b/strokes.go @@ -0,0 +1,95 @@ +package main + +import ( + "errors" + color_lib "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +type Vec2 struct { + x, y float32 +} + +func cursorPosition() Vec2 { + x, y := ebiten.CursorPosition() + return Vec2{ + x: float32(x), + y: float32(y), + } +} + +type stroke struct { + points []Vec2 + width float32 + color color_lib.Color +} + +func (s *stroke) appendIfMoved(point Vec2) { + if len(s.points) != 0 { + lastPoint := s.points[len(s.points)-1] + if lastPoint.x == point.x && lastPoint.y == point.y { + return + } + + } + s.points = append(s.points, point) +} + +func (s *stroke) drawLast(screen *ebiten.Image) error { + if s.color == nil { + return errors.New("stroke has nil color") + } + + if len(s.points) == 0 { + return nil + } + + from := s.points[len(s.points)-1] + vector.DrawFilledCircle(screen, from.x, from.y, s.width/2, s.color, true) + + if len(s.points) == 1 { + return nil + } + + to := s.points[len(s.points)-2] + vector.StrokeLine( + screen, + from.x, + from.y, + to.x, + to.y, + s.width, + s.color, + true, + ) + vector.DrawFilledCircle(screen, to.x, to.y, s.width/2, s.color, true) + + return nil +} + +func (s *stroke) draw(screen *ebiten.Image) error { + if s.color == nil { + return errors.New("stroke has nil color") + } + + from := s.points[0] + vector.DrawFilledCircle(screen, from.x, from.y, s.width/2, s.color, true) + for _, to := range s.points[1:] { + vector.StrokeLine( + screen, + from.x, + from.y, + to.x, + to.y, + s.width, + s.color, + true, + ) + vector.DrawFilledCircle(screen, to.x, to.y, s.width/2, s.color, true) + from = to + } + + return nil +}