From ff9ca3dc1dd7b2ddfa148766df686169b6e32f22 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 8 Jan 2025 15:17:41 +0300 Subject: [PATCH] feat(examples): add space example to measure fps and performance This is a simple example that uses Lip Gloss and Bubble Tea to render a moving color gradient. --- examples/space/main.go | 155 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 examples/space/main.go diff --git a/examples/space/main.go b/examples/space/main.go new file mode 100644 index 0000000000..306944c4d2 --- /dev/null +++ b/examples/space/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + "image/color" + "math/rand" + "os" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" +) + +type fps struct { + frameCount int + lastInstant time.Time + fps *float64 +} + +func (f *fps) tick() { + f.frameCount++ + elapsed := time.Since(f.lastInstant) + // Update FPS every second if we have at least 2 frames + if elapsed > time.Second && f.frameCount > 2 { + fps := float64(f.frameCount) / elapsed.Seconds() + f.fps = &fps + f.frameCount = 0 + f.lastInstant = time.Now() + } +} + +type model struct { + colors [][]color.Color + lastWidth int + lastHeight int + fps fps + frameCount int + width int + height int +} + +func initialModel() model { + return model{ + fps: fps{ + lastInstant: time.Now(), + }, + } +} + +func (m model) Init() (tea.Model, tea.Cmd) { + return m, tea.Batch( + tea.EnterAltScreen, + tickCmd(), + ) +} + +func tickCmd() tea.Cmd { + return tea.Tick(time.Second/60, func(time.Time) tea.Msg { + return tickMsg{} + }) +} + +type tickMsg struct{} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + if m.width != m.lastWidth || m.height != m.lastHeight { + m.setupColors() + m.lastWidth = m.width + m.lastHeight = m.height + } + + case tickMsg: + m.frameCount++ + m.fps.tick() + return m, tickCmd() + } + + return m, nil +} + +func (m *model) setupColors() { + height := m.height * 2 // double height for half blocks + m.colors = make([][]color.Color, height) + + for y := 0; y < height; y++ { + m.colors[y] = make([]color.Color, m.width) + randomnessFactor := float64(height-y) / float64(height) + + for x := 0; x < m.width; x++ { + baseValue := randomnessFactor * (float64(height-y) / float64(height)) + randomOffset := (rand.Float64() * 0.2) - 0.1 + value := clamp(baseValue+randomOffset, 0, 1) + + // Convert value to grayscale color (0-255) + gray := uint8(value * 255) + m.colors[y][x] = lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", gray, gray, gray)) + } + } +} + +func clamp(value, min, max float64) float64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func (m model) View() string { + // Title and FPS display + title := lipgloss.NewStyle().Bold(true).Render("Space") + fpsText := "" + if m.fps.fps != nil { + fpsText = fmt.Sprintf("%.1f fps", *m.fps.fps) + } + header := fmt.Sprintf("%s %s", title, fpsText) + + // Color display + var s strings.Builder + for y := 0; y < m.height; y++ { + for x := 0; x < m.width; x++ { + xi := (x + m.frameCount) % m.width + fg := m.colors[y*2][xi] + bg := m.colors[y*2+1][xi] + st := lipgloss.NewStyle().Foreground(fg).Background(bg) + s.WriteString(st.Render("▀")) + } + s.WriteByte('\n') + } + + return lipgloss.JoinVertical(lipgloss.Left, header, s.String()) +} + +func main() { + p := tea.NewProgram(initialModel(), tea.WithAltScreen(), tea.WithFPS(120)) + + _, err := p.Run() + if err != nil { + fmt.Printf("Error running program: %v", err) + os.Exit(1) + } +}