Add optimization to only redraw if change happened.

This commit is contained in:
Daniel Schmidt 2023-05-19 19:18:18 +02:00
parent 87fafa02e6
commit a303a3a4dd
3 changed files with 195 additions and 57 deletions

View File

@ -13,22 +13,6 @@ func init() {
lipgloss.SetColorProfile(termenv.TrueColor)
}
type fakeEnviron struct{}
func (f fakeEnviron) Environ() []string {
return []string{"TERM", "COLORTERM"}
}
func (f fakeEnviron) Getenv(s string) string {
switch s {
case "TERM":
return "xterm-256color"
case "COLORTERM":
return "truecolor"
}
return ""
}
// Window creates a new crt based bubbletea window with the given width, height, fonts, model and default background color.
// Additional options can be passed to the bubbletea program.
func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg color.Color, options ...tea.ProgramOption) (*crt.Window, *tea.Program, error) {
@ -43,7 +27,7 @@ func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg c
append([]tea.ProgramOption{
tea.WithMouseAllMotion(),
tea.WithInput(gameInput),
tea.WithOutput(termenv.NewOutput(gameOutput, termenv.WithEnvironment(fakeEnviron{}), termenv.WithTTY(true), termenv.WithProfile(termenv.TrueColor), termenv.WithColorCache(true))),
tea.WithOutput(gameOutput),
tea.WithANSICompressor(),
}, options...)...,
)

149
crt.go
View File

@ -42,12 +42,19 @@ type Window struct {
curBg color.Color
curWeight FontWeight
// Callbacks
onUpdate func()
onPreDraw func(screen *ebiten.Image)
onPostDraw func(screen *ebiten.Image)
// Other
showTps bool
fonts Fonts
bgColors *image.RGBA
shader []shader.Shader
routine sync.Once
showTps bool
fonts Fonts
bgColors *image.RGBA
shader []shader.Shader
routine sync.Once
lastBuffer *ebiten.Image
invalidateBuffer bool
}
// NewGame creates a new terminal game with the given dimensions and font faces.
@ -80,19 +87,23 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda
}
game := &Window{
inputAdapter: adapter,
cellsWidth: cellsWidth,
cellsHeight: cellsHeight,
cellWidth: cellWidth,
cellHeight: cellHeight,
cellOffsetY: cellOffsetY,
fonts: fonts,
defaultBg: defaultBg,
grid: grid,
tty: tty,
bgColors: image.NewRGBA(image.Rect(0, 0, cellsWidth*cellWidth, cellsHeight*cellHeight)),
cursorChar: "█",
cursorColor: color.RGBA{R: 255, G: 255, B: 255, A: 100},
inputAdapter: adapter,
cellsWidth: cellsWidth,
cellsHeight: cellsHeight,
cellWidth: cellWidth,
cellHeight: cellHeight,
cellOffsetY: cellOffsetY,
fonts: fonts,
defaultBg: defaultBg,
grid: grid,
tty: tty,
bgColors: image.NewRGBA(image.Rect(0, 0, cellsWidth*cellWidth, cellsHeight*cellHeight)),
cursorChar: "█",
cursorColor: color.RGBA{R: 255, G: 255, B: 255, A: 100},
onUpdate: func() {},
onPreDraw: func(screen *ebiten.Image) {},
onPostDraw: func(screen *ebiten.Image) {},
invalidateBuffer: true,
}
game.inputAdapter.HandleWindowSize(WindowSize{
@ -109,16 +120,19 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda
// SetShowCursor enables or disables the cursor.
func (g *Window) SetShowCursor(val bool) {
g.showCursor = val
g.InvalidateBuffer()
}
// SetCursorChar sets the character that is used for the cursor.
func (g *Window) SetCursorChar(char string) {
g.cursorChar = char
g.InvalidateBuffer()
}
// SetCursorColor sets the color of the cursor.
func (g *Window) SetCursorColor(color color.Color) {
g.cursorColor = color
g.InvalidateBuffer()
}
// SetShader sets a shader that is applied to the whole screen.
@ -126,11 +140,31 @@ func (g *Window) SetShader(shader ...shader.Shader) {
g.shader = shader
}
// SetOnUpdate sets a function that is called every frame.
func (g *Window) SetOnUpdate(fn func()) {
g.onUpdate = fn
}
// SetOnPreDraw sets a function that is called before the screen is drawn.
func (g *Window) SetOnPreDraw(fn func(screen *ebiten.Image)) {
g.onPreDraw = fn
}
// SetOnPostDraw sets a function that is called after the screen is drawn.
func (g *Window) SetOnPostDraw(fn func(screen *ebiten.Image)) {
g.onPostDraw = fn
}
// ShowTPS enables or disables the TPS counter on the top left.
func (g *Window) ShowTPS(val bool) {
g.showTps = val
}
// InvalidateBuffer forces the buffer to be redrawn.
func (g *Window) InvalidateBuffer() {
g.invalidateBuffer = true
}
// ResetSGR resets the SGR attributes to their default values.
func (g *Window) ResetSGR() {
g.curFg = color.White
@ -145,6 +179,17 @@ func (g *Window) SetBgPixels(x, y int, c color.Color) {
g.bgColors.Set(x*g.cellWidth+i, y*g.cellHeight+j, c)
}
}
g.InvalidateBuffer()
}
// GetCellsWidth returns the number of cells in the x direction.
func (g *Window) GetCellsWidth() int {
return g.cellsWidth
}
// GetCellsHeight returns the number of cells in the y direction.
func (g *Window) GetCellsHeight() int {
return g.cellsHeight
}
func (g *Window) handleCSI(csi any) {
@ -286,6 +331,7 @@ func (g *Window) parseSequences(str string, printExtra bool) int {
lastFound = i
for i := range sgr {
g.handleSGR(sgr[i])
g.InvalidateBuffer()
}
}
} else if csi, ok := extractCSI(string(runes[i:])); ok {
@ -294,11 +340,13 @@ func (g *Window) parseSequences(str string, printExtra bool) int {
if csi, ok := parseCSI(csi); ok {
lastFound = i
g.handleCSI(csi)
g.InvalidateBuffer()
}
} else if printExtra {
g.PrintChar(runes[i], g.curFg, g.curBg, g.curWeight)
}
}
return lastFound
}
@ -356,6 +404,8 @@ func (g *Window) PrintChar(r rune, fg, bg color.Color, weight FontWeight) {
// Move the cursor.
g.cursorX++
g.InvalidateBuffer()
}
func (g *Window) Update() error {
@ -438,6 +488,8 @@ func (g *Window) Update() error {
// Keyboard.
g.inputAdapter.HandleKeyPress()
g.onUpdate()
return nil
}
@ -447,39 +499,54 @@ func (g *Window) Draw(screen *ebiten.Image) {
screen.Fill(g.defaultBg)
bufferImage := ebiten.NewImage(g.cellsWidth*g.cellWidth, g.cellsHeight*g.cellHeight)
g.onPreDraw(screen)
// Draw background
bufferImage.WritePixels(g.bgColors.Pix)
// Get current buffer
bufferImage := g.lastBuffer
// Draw text
for y := 0; y < g.cellsHeight; y++ {
for x := 0; x < g.cellsWidth; x++ {
if g.grid[y][x].Char == ' ' {
continue
}
// Only draw the buffer if it's invalid
if bufferImage == nil || g.invalidateBuffer {
bufferImage = ebiten.NewImage(g.cellsWidth*g.cellWidth, g.cellsHeight*g.cellHeight)
switch g.grid[y][x].Weight {
case FontWeightNormal:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Normal, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightBold:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Bold, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightItalic:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Italic, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
// Draw background
bufferImage.WritePixels(g.bgColors.Pix)
// Draw text
for y := 0; y < g.cellsHeight; y++ {
for x := 0; x < g.cellsWidth; x++ {
if g.grid[y][x].Char == ' ' {
continue
}
switch g.grid[y][x].Weight {
case FontWeightNormal:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Normal, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightBold:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Bold, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightItalic:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Italic, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
}
}
}
// Draw cursor
if g.showCursor {
text.Draw(bufferImage, g.cursorChar, g.fonts.Normal, g.cursorX*g.cellWidth, g.cursorY*g.cellHeight+g.cellOffsetY, g.cursorColor)
}
g.lastBuffer = bufferImage
g.invalidateBuffer = false
}
if g.showCursor {
text.Draw(bufferImage, g.cursorChar, g.fonts.Normal, g.cursorX*g.cellWidth, g.cursorY*g.cellHeight+g.cellOffsetY, g.cursorColor)
}
// Draw shader
if g.shader != nil {
shaderBuffer := ebiten.NewImageFromImage(bufferImage)
for i := range g.shader {
_ = g.shader[i].Apply(screen, bufferImage)
_ = g.shader[i].Apply(screen, shaderBuffer)
if len(g.shader) > 0 {
bufferImage.DrawImage(screen, nil)
shaderBuffer.DrawImage(screen, nil)
}
}
} else {
@ -489,6 +556,8 @@ func (g *Window) Draw(screen *ebiten.Image) {
if g.showTps {
ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS()))
}
g.onPostDraw(screen)
}
func (g *Window) Layout(outsideWidth, outsideHeight int) (int, int) {

View File

@ -0,0 +1,85 @@
package main
import (
"flag"
"fmt"
"github.com/BigJk/crt"
bubbleadapter "github.com/BigJk/crt/bubbletea"
"github.com/BigJk/crt/shader"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/hajimehoshi/ebiten/v2"
"image/color"
"math/rand"
"time"
)
const (
Width = 1000
Height = 600
)
type model struct {
X, Y int
}
func (m *model) Init() tea.Cmd {
return nil
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *model) View() string {
return lipgloss.NewStyle().Margin(m.X, 0, 0, m.Y).Padding(5).Border(lipgloss.ThickBorder(), true).Background(lipgloss.Color("#fc2022")).Foreground(lipgloss.Color("#ff00ff")).Render("Hello World!")
}
func main() {
enableShader := flag.Bool("shader", false, "Enable shader")
flag.Parse()
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 8.0)
if err != nil {
panic(err)
}
mod := &model{}
win, prog, err := bubbleadapter.Window(Width, Height, fonts, mod, color.Black, tea.WithAltScreen())
if err != nil {
panic(err)
}
go func() {
for {
mod.X = rand.Intn(win.GetCellsWidth())
mod.Y = rand.Intn(win.GetCellsHeight())
prog.Send(time.Now())
time.Sleep(time.Second)
}
}()
var lastStart int64
win.SetOnPreDraw(func(screen *ebiten.Image) {
lastStart = time.Now().UnixMicro()
})
win.SetOnPostDraw(func(screen *ebiten.Image) {
elapsed := time.Now().UnixMicro() - lastStart
fmt.Printf("Frame took %d micro seconds FPS=%.2f\n", elapsed, 1000/(float64(elapsed)*0.001))
})
if *enableShader {
lotte, err := shader.NewCrtLotte()
if err != nil {
panic(err)
}
win.SetShader(lotte)
}
if err := win.Run("Simple"); err != nil {
panic(err)
}
}