From ee5ab2ed4935bf68b6bf8052f7137c20f66e1b05 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Fri, 22 Oct 2021 02:20:09 -0700 Subject: [PATCH] Support panning around canvas --- .gitignore | 2 + application/application.go | 167 +++++++++++++++++++++++++++++-------- application/canvas.go | 6 +- flags.go | 40 +++++++++ flags_web.go | 12 +++ goreleaser.yml | 31 +++++++ main.go | 2 + 7 files changed, 222 insertions(+), 38 deletions(-) create mode 100644 flags.go create mode 100644 flags_web.go create mode 100644 goreleaser.yml diff --git a/.gitignore b/.gitignore index 968bc4b..282c099 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea *.sh +dist +edbit diff --git a/application/application.go b/application/application.go index 25f6590..9229e93 100644 --- a/application/application.go +++ b/application/application.go @@ -1,15 +1,21 @@ package application import ( + "errors" "fmt" "image" "image/color" + "image/gif" + "image/jpeg" + "image/png" + "log" "math" "os" - - "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "strings" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "golang.org/x/image/bmp" "golang.org/x/image/colornames" ) @@ -22,6 +28,8 @@ type Application struct { canvases []*canvas + active int + spinnerIndex int hoverImg *ebiten.Image @@ -30,35 +38,95 @@ type Application struct { } func NewApplication() *Application { - a := &Application{ + app := &Application{ op: &ebiten.DrawImageOptions{}, } - a.addCanvas(8, 8) + app.addNewCanvas(8, 8) - a.canvases[0].img.Fill(colornames.Purple) + c := app.activeCanvas() + c.img.Fill(colornames.Purple) + c.scale = 256 - a.activeCanvas().scale = 256 - a.hoverImg = ebiten.NewImage(int(a.canvases[0].scale), int(a.canvases[0].scale)) // TODO - a.drawHoverImage() + app.drawHoverImage() - return a + return app } -func (app *Application) addCanvas(w, h int) { +func (app *Application) readFile(p string) (image.Image, error) { + f, err := os.OpenFile(p, os.O_RDONLY, 0600) + if err != nil { + log.Fatal(err) + } + defer f.Close() + p = strings.ToLower(p) + if strings.HasSuffix(p, ".bmp") { + img, err := bmp.Decode(f) + if err != nil { + log.Fatalf("failed to decode BMP file: %s", err) + } + return img, nil + } else if strings.HasSuffix(p, ".gif") { + img, err := gif.Decode(f) + if err != nil { + log.Fatalf("failed to decode GIF file: %s", err) + } + return img, nil + } else if strings.HasSuffix(p, ".jpg") || strings.HasSuffix(p, ".jpeg") { + img, err := jpeg.Decode(f) + if err != nil { + log.Fatalf("failed to decode JPG file: %s", err) + } + return img, nil + } else if strings.HasSuffix(p, ".png") { + img, err := png.Decode(f) + if err != nil { + log.Fatalf("failed to decode PNG file: %s", err) + } + return img, nil + } else if strings.HasSuffix(p, ".tff") || strings.HasSuffix(p, ".tiff") { + img, err := png.Decode(f) + if err != nil { + log.Fatalf("failed to decode PNG file: %s", err) + } + return img, nil + } + return nil, errors.New("unknown image format") +} + +func (app *Application) OpenFile(p string) { + img, err := app.readFile(p) + if err != nil { + log.Fatal(err) + } + c := NewCanvas(img.Bounds().Dx(), img.Bounds().Dy()) + c.img = ebiten.NewImageFromImage(img) + app.addCanvas(c) +} + +func (app *Application) addCanvas(c *canvas) { + app.canvases = append(app.canvases, c) + app.active++ + + app.drawHoverImage() +} + +func (app *Application) addNewCanvas(w, h int) { c := NewCanvas(w, h) app.canvases = append(app.canvases, c) + + app.drawHoverImage() } func (app *Application) activeCanvas() *canvas { - return app.canvases[0] + return app.canvases[app.active] } func (app *Application) screenToCanvas(x, y int) (int, int) { c := app.activeCanvas() - - cx, cy := int(float64(x)/c.scale), int(float64(y)/c.scale) - return cx, cy + cx, cy := float64(x)/c.scale, float64(y)/c.scale + cx, cy = cx+c.x, cy+c.y + return int(cx), int(cy) } func (app *Application) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { @@ -86,20 +154,22 @@ func (app *Application) Update() error { scroll = 1 } - offset := scroll * (app.canvases[0].scale / 7) - app.canvases[0].scale += offset - if app.canvases[0].scale < .001 { - app.canvases[0].scale = .001 - } else if app.canvases[0].scale > 1000 { - app.canvases[0].scale = 1000 + c := app.activeCanvas() + + offset := scroll * (c.scale / 7) + c.scale += offset + if c.scale < .001 { + c.scale = .001 + } else if c.scale > 1000 { + c.scale = 1000 } - hoverSize := int(math.Ceil(app.canvases[0].scale)) - if hoverSize < 1 { - hoverSize = 1 + if c.scale > 10 { + c.scale = float64(int(c.scale)) + } else { + c.scale = math.Trunc(c.scale*10000) / 10000 } - hoverSize += 2 - app.hoverImg = ebiten.NewImage(hoverSize, hoverSize) + app.drawHoverImage() } @@ -111,29 +181,43 @@ func (app *Application) Update() error { c.img.Set(cx, cy, colornames.White) } + c := app.activeCanvas() + panSize := 2 / c.scale + if ebiten.IsKeyPressed(ebiten.KeyRight) { + c.x += panSize + } + if ebiten.IsKeyPressed(ebiten.KeyLeft) { + c.x -= panSize + } + if ebiten.IsKeyPressed(ebiten.KeyUp) { + c.y -= panSize + } + if ebiten.IsKeyPressed(ebiten.KeyDown) { + c.y += panSize + } return nil } func (app *Application) Draw(screen *ebiten.Image) { - // TODO translate and scale by camera pos + // Draw canvas. c := app.activeCanvas() app.op.GeoM.Reset() + app.op.GeoM.Translate(-c.x, -c.y) app.op.GeoM.Scale(c.scale, c.scale) screen.DrawImage(c.img, app.op) + // Draw hover outline. cx, cy := app.screenToCanvas(app.cursorX, app.cursorY) p := c.pixelScreenRect(cx, cy) - app.op.GeoM.Reset() - app.op.GeoM.Translate(float64(p.Min.X)-1, float64(p.Min.Y)-1) + app.op.GeoM.Translate(float64(p.Min.X), float64(p.Min.Y)) screen.DrawImage(app.hoverImg, app.op) + // Print debug information. debugInfo := fmt.Sprintf("SCA %0.2f\nFPS %c %0.0f", app.activeCanvas().scale, spinner[app.spinnerIndex], ebiten.CurrentFPS()) - debugBox := image.NewRGBA(image.Rect(10, 20, 200, 200)) debugImg := ebiten.NewImageFromImage(debugBox) ebitenutil.DebugPrint(debugImg, debugInfo) - app.op.GeoM.Reset() app.op.GeoM.Translate(3, 0) app.op.GeoM.Scale(2, 2) @@ -146,6 +230,15 @@ func (app *Application) Draw(screen *ebiten.Image) { } func (app *Application) drawHoverImage() { + c := app.activeCanvas() + hoverImgSize := int(c.scale) + if hoverImgSize < 1 { + hoverImgSize = 1 + } + if app.hoverImg == nil || app.hoverImg.Bounds().Dx() != hoverImgSize || app.hoverImg.Bounds().Dy() != hoverImgSize { + app.hoverImg = ebiten.NewImage(hoverImgSize, hoverImgSize) + } + var colorWhite bool nextColor := func() color.Color { colorWhite = !colorWhite @@ -160,24 +253,24 @@ func (app *Application) drawHoverImage() { img.Clear() bounds := img.Bounds() - hoverSize := 2 + outlineWidth := 2 if app.activeCanvas().scale >= 1000 { - hoverSize = 6 + outlineWidth = 6 } else if app.activeCanvas().scale >= 200 { - hoverSize = 5 + outlineWidth = 5 } else if app.activeCanvas().scale >= 100 { - hoverSize = 4 + outlineWidth = 4 } else if app.activeCanvas().scale >= 10 { - hoverSize = 3 + outlineWidth = 3 } - for y := 0; y < hoverSize; y++ { + for y := 0; y < outlineWidth; y++ { for x := 0; x < bounds.Max.X; x++ { c := nextColor() img.Set(x, bounds.Min.Y+y, c) img.Set(x, bounds.Max.Y-y, c) } } - for x := 0; x < hoverSize; x++ { + for x := 0; x < outlineWidth; x++ { for y := 0; y < bounds.Max.Y; y++ { c := nextColor() img.Set(bounds.Min.X+x, y, c) diff --git a/application/canvas.go b/application/canvas.go index 30f3aee..14bef68 100644 --- a/application/canvas.go +++ b/application/canvas.go @@ -10,6 +10,8 @@ import ( type canvas struct { w, h int + x, y float64 // View position + img *ebiten.Image scale float64 @@ -25,5 +27,7 @@ func NewCanvas(w, h int) *canvas { } func (c *canvas) pixelScreenRect(x, y int) image.Rectangle { - return image.Rect(int(math.Floor(float64(x)*c.scale)), int(math.Floor(float64(y)*c.scale)), int(math.Ceil(float64(x+1)*c.scale)), int(math.Ceil(float64(y+1)*c.scale))) + rx, ry := (float64(x)-c.x)*c.scale, (float64(y)-c.y)*c.scale + rxCorner, ryCorner := (float64(x+1)-c.x)*c.scale, (float64(y+1)-c.y)*c.scale + return image.Rect(int(math.Floor(rx)), int(math.Floor(ry)), int(math.Ceil(rxCorner)), int(math.Ceil(ryCorner))) } diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..2371663 --- /dev/null +++ b/flags.go @@ -0,0 +1,40 @@ +//go:build !js && !wasm +// +build !js,!wasm + +package main + +import ( + "flag" + "log" + "os" + "path/filepath" + "strings" + + "code.rocketnine.space/tslocum/edbit/application" +) + +func parseFlags(app *application.Application) { + flag.Parse() + + args := flag.Args() + for _, p := range args { + if strings.TrimSpace(p) == "" { + continue + } + + if p == "~" { + dir, err := os.UserHomeDir() + if err != nil { + log.Fatal(err) + } + p = dir + } else if strings.HasPrefix(p, "~/") { + dir, err := os.UserHomeDir() + if err != nil { + log.Fatal(err) + } + p = filepath.Join(dir, p[2:]) + } + app.OpenFile(p) + } +} diff --git a/flags_web.go b/flags_web.go new file mode 100644 index 0000000..b7a2dec --- /dev/null +++ b/flags_web.go @@ -0,0 +1,12 @@ +//go:build js || wasm +// +build js wasm + +package main + +import ( + "code.rocketnine.space/tslocum/edbit/application" +) + +func parseFlags(app *application.Application) { + // Do nothing +} diff --git a/goreleaser.yml b/goreleaser.yml new file mode 100644 index 0000000..1a6c774 --- /dev/null +++ b/goreleaser.yml @@ -0,0 +1,31 @@ +project_name: edbit + +builds: + - + id: edbit +# ldflags: +# - -s -w -X code.rocketnine.space/tslocum/edbit/main.Version={{.Version}} + goos: + - js + - linux + - windows + goarch: + - amd64 + - wasm +archives: + - + id: edbit + builds: + - edbit + replacements: + 386: i386 + format_overrides: + - goos: js + format: zip + - goos: windows + format: zip + files: + - ./*.md + - LICENSE +checksum: + name_template: 'checksums.txt' diff --git a/main.go b/main.go index ffedca7..ebd68ab 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,8 @@ func main() { app.Exit() }() + parseFlags(app) + if err := ebiten.RunGame(app); err != nil { log.Fatal(err) }