From ac5c72ddb345d117221f29111f621240d2bb2711 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 7 Oct 2024 14:15:16 -0700 Subject: [PATCH] [life]: left, left drag to set cells; right click to erase and support for that in ansipixels. Also WriteBoxed handle multi lines (#64) * life: left/right clicks. WriteBoxed handle multi line * More help and cycle on 1/2 pixel * Adding leftclick drag paint to life --- ansipixels/ansipixels.go | 19 +++-- ansipixels/mouse.go | 22 +++++ life/life.go | 171 ++++++++++++++++++++++++++++++++------- 3 files changed, 179 insertions(+), 33 deletions(-) diff --git a/ansipixels/ansipixels.go b/ansipixels/ansipixels.go index 24106fc..4500bdc 100644 --- a/ansipixels/ansipixels.go +++ b/ansipixels/ansipixels.go @@ -416,11 +416,20 @@ func (ap *AnsiPixels) DrawBox(x, y, w, h int, topLeft, topRight, bottomLeft, bot func (ap *AnsiPixels) WriteBoxed(y int, msg string, args ...interface{}) { s := fmt.Sprintf(msg, args...) - w := ap.ScreenWidth(s) - x := (ap.W - w) / 2 - ap.MoveCursor(x, y) - ap.WriteString(s) - ap.DrawRoundBox(x-1, y-1, w+2, 3) + lines := strings.Split(s, "\n") + maxw := 0 + widths := make([]int, 0, len(lines)) + for _, l := range lines { + w := ap.ScreenWidth(l) + widths = append(widths, w) + maxw = max(maxw, w) + } + for i, l := range lines { + x := (ap.W - widths[i]) / 2 + ap.MoveCursor(x, y+i) + ap.WriteString(l) + } + ap.DrawRoundBox((ap.W-maxw)/2-1, y-1, maxw+2, len(lines)+2) } func (ap *AnsiPixels) WriteRightBoxed(y int, msg string, args ...interface{}) { diff --git a/ansipixels/mouse.go b/ansipixels/mouse.go index 7aaf911..790c105 100644 --- a/ansipixels/mouse.go +++ b/ansipixels/mouse.go @@ -60,3 +60,25 @@ func (ap *AnsiPixels) MouseDecode() { ap.MouseDecode() ap.Mouse = true } + +const ( + MouseLeft = 0 + MouseRight = 0b10 + MouseMove = 0b100000 +) + +func (ap *AnsiPixels) LeftClick() bool { + return ap.Mouse && (ap.Mbuttons == MouseLeft) +} + +func (ap *AnsiPixels) RightClick() bool { + return ap.Mouse && (ap.Mbuttons == MouseRight) +} + +func (ap *AnsiPixels) LeftDrag() bool { + return ap.Mouse && (ap.Mbuttons == MouseMove|MouseLeft) +} + +func (ap *AnsiPixels) RightDrag() bool { + return ap.Mouse && (ap.Mbuttons == MouseMove|MouseRight) +} diff --git a/life/life.go b/life/life.go index 652fa70..1bd61c8 100644 --- a/life/life.go +++ b/life/life.go @@ -42,10 +42,18 @@ func (c *Conway) Set(x, y int) { c.Cells[1-c.Current][y*c.Width+x] = 1 } +func (c *Conway) SetCurrent(x, y int) { + c.Cells[c.Current][y*c.Width+x] = 1 +} + func (c *Conway) Clear(x, y int) { c.Cells[1-c.Current][y*c.Width+x] = 0 } +func (c *Conway) ClearCurrent(x, y int) { + c.Cells[c.Current][y*c.Width+x] = 0 +} + func (c *Conway) Copy(x, y int) { idx := y*c.Width + x c.Cells[1-c.Current][idx] = c.Cells[c.Current][idx] @@ -125,57 +133,164 @@ func Draw(ap *ansipixels.AnsiPixels, c *Conway) { } } +type GameState string + +const ( + Paused = "Paused" + Running = "Running" +) + +type Game struct { + ap *ansipixels.AnsiPixels + c *Conway + state GameState + showInfo bool + showHelp bool + generation uint64 + lastClickX, lastClickY int + delta int // which 1/2 pixel we're targeting with the mouse. + lastWasClick bool +} + func Main() int { fpsFlag := flag.Float64("fps", 60, "Frames per second") flagRandomFill := flag.Float64("fill", 0.1, "Random fill factor (0 to 1)") flagGlider := flag.Bool("glider", false, "Start with a glider (default is random)") cli.Main() + game := &Game{} ap := ansipixels.NewAnsiPixels(*fpsFlag) err := ap.Open() if err != nil { return log.FErrf("Error opening AnsiPixels: %v", err) } - defer ap.Restore() + game.ap = ap + defer game.End() ap.HideCursor() - var generation uint64 - var c *Conway + ap.MouseTrackingOn() // needed for drag, other ap.MouseClickOn() is enough. fillFactor := float32(*flagRandomFill) ap.OnResize = func() error { - c = NewConway(ap.W, 2*ap.H) // half pixels vertically. + game.c = NewConway(ap.W, 2*ap.H) // half pixels vertically. if *flagGlider { - c.Glider(ap.W/3, 2*ap.H/3) // first third of the screen + game.c.Glider(ap.W/3, 2*ap.H/3) // first third of the screen } else { // Random - c.Randomize(fillFactor) + game.c.Randomize(fillFactor) } - c.Current = 1 - c.Current - generation = 0 + game.c.Current = 1 - game.c.Current + game.generation = 1 + game.showInfo = true + game.state = Paused + game.showHelp = true + game.delta = 0 + game.DrawOne() return nil } _ = ap.OnResize() - showInfo := true for { - ap.StartSyncMode() - ap.ClearScreen() - if showInfo { - ap.WriteRight(ap.H-1, "FPS %.0f Generation: %d ", ap.FPS, generation) + switch game.state { + case Running: + _, err := ap.ReadOrResizeOrSignalOnce() + if err != nil { + return log.FErrf("Error reading: %v", err) + } + case Paused: + err := ap.ReadOrResizeOrSignal() + if err != nil { + return log.FErrf("Error reading: %v", err) + } } - Draw(ap, c) - generation++ - ap.EndSyncMode() - n, err := ap.ReadOrResizeOrSignalOnce() - if err != nil { - return log.FErrf("Error reading: %v", err) + if ap.Mouse { + game.HandleMouse() + continue } - if n > 0 { - switch ap.Data[0] { - case 'q', 'Q', 3: - ap.MoveCursor(0, 0) - return 0 - case 'i', 'I': - showInfo = !showInfo - } + if len(ap.Data) == 0 { + game.Next() + continue + } + switch ap.Data[0] { + case 'q', 'Q', 3: + return 0 + case 'i', 'I': + game.showInfo = !game.showInfo + case '?', 'h', 'H': + game.showHelp = true + game.state = Paused + case ' ': + game.state = Paused + default: + game.state = Running } - c.Next() + game.Next() + } +} + +func (g *Game) DrawOne() { + g.ap.StartSyncMode() + g.ap.ClearScreen() + if g.showInfo { + g.ap.WriteRight(g.ap.H-1, "%s FPS %.0f Generation: %d ", g.state, g.ap.FPS, g.generation) + } + Draw(g.ap, g.c) + if g.showHelp { + g.ap.WriteBoxed(g.ap.H/2+2, "Space to pause, q to quit, i for info, other key to run\n"+ + "Left click or hold to set, right click to clear\nClick in same spot for other half pixel") + g.showHelp = false + } + g.ap.EndSyncMode() +} + +func (g *Game) Next() { + g.c.Next() + g.generation++ + g.DrawOne() +} + +func (g *Game) End() { + g.ap.MouseTrackingOff() // g.ap.MouseClickOff() + g.ap.ShowCursor() + g.ap.MoveCursor(0, g.ap.H-2) + g.ap.Restore() +} + +func (g *Game) HandleMouse() { + // maybe we need a different delta for left and right clicks + // but for now it's pretty good to cycle a pixels' 2 halves. (2 left clicks, 2 right clicks) + delta := 0 + sameSpot := g.ap.Mx == g.lastClickX && g.ap.My == g.lastClickY + prevWasClick := g.lastWasClick + leftDrag := g.ap.LeftDrag() + ld := prevWasClick && leftDrag && !sameSpot + if sameSpot { + delta = 1 - g.delta + } else if ld { + delta = g.delta + } + g.lastWasClick = false + switch { + case g.ap.LeftClick(), ld: + log.LogVf("Mouse left (%06b) click (drag %t) at %d, %d", g.ap.Mbuttons, ld, g.ap.Mx, g.ap.My) + g.c.SetCurrent(g.ap.Mx-1, (g.ap.My-1)*2+delta) + g.lastWasClick = true + if ld { + g.DrawOne() + return + } + case g.ap.RightClick(): + log.LogVf("Mouse right (%06b) click (drag %t) at %d, %d", g.ap.Mbuttons, leftDrag, g.ap.Mx, g.ap.My) + g.c.ClearCurrent(g.ap.Mx-1, (g.ap.My-1)*2+delta) + g.lastWasClick = true + default: + log.LogVf("Mouse %06b at %d, %d last was click %t same spot %t left drag %t", + g.ap.Mbuttons, g.ap.Mx, g.ap.My, + prevWasClick, sameSpot, leftDrag) + return + } + if sameSpot { + g.delta = 1 - g.delta + } else { + g.delta = 0 } + g.lastClickX = g.ap.Mx + g.lastClickY = g.ap.My + g.DrawOne() }