From 3765727e659402b7901f8e739f769db567404421 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 12 Dec 2022 09:52:00 -0500 Subject: [PATCH] docs(examples): add cellbuffer + harmonica example (#409) --- examples/cellbuffer/main.go | 199 ++++++++++++++++++++++++++++++++++++ examples/go.mod | 1 + 2 files changed, 200 insertions(+) create mode 100644 examples/cellbuffer/main.go diff --git a/examples/cellbuffer/main.go b/examples/cellbuffer/main.go new file mode 100644 index 0000000..00de39c --- /dev/null +++ b/examples/cellbuffer/main.go @@ -0,0 +1,199 @@ +package main + +// A simple example demonstrating how to draw and animate on a cellular grid. +// Note that the cellbuffer implementation in this example does not support +// double-width runes. + +import ( + "fmt" + "os" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/harmonica" +) + +const ( + fps = 60 + frequency = 7.5 + damping = 0.15 +) + +func drawEllipse(cb *cellbuffer, xc, yc, rx, ry float64) { + const c = "*" + var ( + dx, dy, d1, d2 float64 + x float64 = 0 + y = ry + ) + + d1 = ry*ry - rx*rx*ry + 0.25*rx*rx + dx = 2 * ry * ry * x + dy = 2 * rx * rx * y + + for dx < dy { + cb.set(c, int(x+xc), int(y+yc)) + cb.set(c, int(-x+xc), int(y+yc)) + cb.set(c, int(x+xc), int(-y+yc)) + cb.set(c, int(-x+xc), int(-y+yc)) + if d1 < 0 { + x++ + dx = dx + (2 * ry * ry) + d1 = d1 + dx + (ry * ry) + } else { + x++ + y-- + dx = dx + (2 * ry * ry) + dy = dy - (2 * rx * rx) + d1 = d1 + dx - dy + (ry * ry) + } + } + + d2 = ((ry * ry) * ((x + 0.5) * (x + 0.5))) + ((rx * rx) * ((y - 1) * (y - 1))) - (rx * rx * ry * ry) + + for y >= 0 { + cb.set(c, int(x+xc), int(y+yc)) + cb.set(c, int(-x+xc), int(y+yc)) + cb.set(c, int(x+xc), int(-y+yc)) + cb.set(c, int(-x+xc), int(-y+yc)) + if d2 > 0 { + y-- + dy = dy - (2 * rx * rx) + d2 = d2 + (rx * rx) - dy + } else { + y-- + x++ + dx = dx + (2 * ry * ry) + dy = dy - (2 * rx * rx) + d2 = d2 + dx - dy + (rx * rx) + } + } +} + +type cellbuffer struct { + cells []string + stride int +} + +func (c *cellbuffer) init(w, h int) { + if w == 0 { + return + } + c.stride = w + c.cells = make([]string, w*h) + c.wipe() +} + +func (c cellbuffer) set(v string, x, y int) { + i := y*c.stride + x + if i > len(c.cells)-1 || x < 0 || y < 0 || x >= c.width() || y >= c.height() { + return + } + c.cells[i] = v +} + +func (c *cellbuffer) clear(x, y int) { + c.set(" ", x, y) +} + +func (c *cellbuffer) wipe() { + for i := range c.cells { + c.cells[i] = " " + } +} + +func (c cellbuffer) width() int { + return c.stride +} + +func (c cellbuffer) height() int { + h := len(c.cells) / c.stride + if len(c.cells)%c.stride != 0 { + h++ + } + return h +} + +func (c cellbuffer) ready() bool { + return len(c.cells) > 0 +} + +func (c cellbuffer) String() string { + var b strings.Builder + for i := 0; i < len(c.cells); i++ { + if i > 0 && i%c.stride == 0 && i < len(c.cells)-1 { + b.WriteRune('\n') + } + b.WriteString(c.cells[i]) + } + return b.String() +} + +type frameMsg struct{} + +func animate() tea.Cmd { + return tea.Tick(time.Second/fps, func(_ time.Time) tea.Msg { + return frameMsg{} + }) +} + +type model struct { + cells cellbuffer + spring harmonica.Spring + targetX, targetY float64 + x, y float64 + xVelocity, yVelocity float64 +} + +func (m model) Init() tea.Cmd { + return animate() +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + return m, tea.Quit + case tea.WindowSizeMsg: + if !m.cells.ready() { + m.targetX, m.targetY = float64(msg.Width)/2, float64(msg.Height)/2 + } + m.cells.init(msg.Width, msg.Height) + return m, nil + case tea.MouseMsg: + if !m.cells.ready() { + return m, nil + } + m.targetX, m.targetY = float64(msg.X), float64(msg.Y) + return m, nil + + case frameMsg: + if !m.cells.ready() { + return m, nil + } + + m.cells.wipe() + m.x, m.xVelocity = m.spring.Update(m.x, m.xVelocity, m.targetX) + m.y, m.yVelocity = m.spring.Update(m.y, m.yVelocity, m.targetY) + drawEllipse(&m.cells, m.x, m.y, 16, 8) + return m, animate() + default: + return m, nil + } +} + +func (m model) View() string { + return m.cells.String() +} + +func main() { + m := model{ + spring: harmonica.NewSpring(harmonica.FPS(fps), frequency, damping), + } + + p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) + if _, err := p.Run(); err != nil { + fmt.Println("Uh oh:", err) + os.Exit(1) + } +} diff --git a/examples/go.mod b/examples/go.mod index 7465247..8804897 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -6,6 +6,7 @@ require ( github.com/charmbracelet/bubbles v0.13.1-0.20220815142520-649f78e1fd8b github.com/charmbracelet/bubbletea v0.22.0 github.com/charmbracelet/glamour v0.6.0 + github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/lipgloss v0.6.0 github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 github.com/lucasb-eyer/go-colorful v1.2.0