package render import ( "io" "slices" "time" ) const ( // bufSize how many instruction a buffer should be capable of holding // before blocking. Increase this if you have a lot of simultaneous // instructions, but be aware that this will increase memory usage. bufSize = 9999 // how many times per second the renderer should refresh the screen. refreshRate = 60 ) // Renderer is used to constantly draw onto the terminal. // A lazy method is used to draw, where the renderer will only draw // parts of the screen which it has been instructed to draw. Also, // the renderer draws at a constant rate, so every object on screen // is drawn at the same rate/predictably. // // To achieve this, the renderer uses two channels that essentially // act as stacks (they are private so only push/pop methods can be used). // Whenever a new frame is to be drawn, the renderer makes copies over // every instruction in it's stacks to temporary buffers (to avoid blocking) // and proceeds with the drawing. type Renderer struct { refreshRate int // both stacks are only externally accessible through the Push method // which ensures each instruction is sent to the correct stack. drwStack chan Instruction clrStack chan Instruction stream io.Writer ticker *time.Ticker done chan bool } func NewRenderer(stream io.Writer) *Renderer { return &Renderer{ refreshRate: refreshRate, drwStack: make(chan Instruction, bufSize), clrStack: make(chan Instruction, bufSize), stream: stream, } } func (r *Renderer) ClearScreen() { io.WriteString(r.stream, "\033[2J") } func (r *Renderer) Start() { r.ClearScreen() defer io.WriteString(r.stream, "\033[?25h") // show cursor r.ticker = time.NewTicker(time.Duration(float64(time.Second) / float64(r.refreshRate))) r.done = make(chan bool) go func() { for { select { case <-r.done: return case <-r.ticker.C: r.DrawFrame() } } }() } func (r *Renderer) Stop() { r.ticker.Stop() r.done <- true // Reset done channel to allow for a new Start r.done = nil } func (r *Renderer) bufferize(stack chan Instruction) []Instruction { var buf []Instruction for { select { case i := <-stack: buf = append(buf, i) default: return buf } } } func (r *Renderer) Push(i Instruction) { if i == nil { // fmt.Println("nil instruction") return } if r.drwStack == nil || r.clrStack == nil { // fmt.Println("renderer not started") return } // fmt.Printf("Pushing instruction: %T\n", i) switch inst := i.(type) { case DrawInstruction: // fmt.Println("pushing draw") r.drwStack <- inst // fmt.Println("pushed draw") case ClearInstruction: // fmt.Println("pushing clear") r.clrStack <- inst // fmt.Println("pushed clear") default: // fmt.Println("unsupported instruction type") } } // DrawFrame pushes all instructions from the stacks to temporary buffers // and proceeds with the drawing. Before doing so, it must sanitize certain // components: // - If a ClearInstruction overlaps with a DrawInstruction, the ClearInstruction // part that overlaps is squashed (resize + move). An example of why this is // necessary is if an object is chasing another. The chased object will send // instructions to clear it's trail, but if the chaser is faster, it must not // get cleared. Otherwise, the chaser would be invisible. // - If two DrawInstructions overlap, the latest instruction is prioritized // This avoids unnecessary clearing and redrawing. func (r *Renderer) DrawFrame() { // NOTE: Buffer indices closer to zero are older instructions // as they were the first pushed to the stack. // Create buffers drwBuf := r.bufferize(r.drwStack) clrBuf := r.bufferize(r.clrStack) var stack []Instruction // fmt.Println(len(drwBuf)) // fmt.Println(len(clrBuf)) // squash overlapping clear instructions for _, drw := range drwBuf { for j, clr := range clrBuf { if ot := overlap(drw, clr); ot != CoverNone { newClr := squash(drw, clr, ot) switch len(newClr) { case 0: // Complete overlap -> delete the one under clrBuf = slices.Delete(clrBuf, j, j) case 1: // Partial overlap -> resize the one under clrBuf[j] = newClr[0] default: // Over splits the one under in two -> delete and append parts clrBuf = slices.Delete(clrBuf, j, j) clrBuf = append(clrBuf, newClr...) } } } } // squash overlapping draw instructions for i := 0; i < len(drwBuf); i++ { for j := i + 1; j < len(drwBuf); j++ { older, newer := drwBuf[i], drwBuf[j] if ot := overlap(older, newer); ot != CoverNone { newDrw := squash(newer, older, ot) switch len(newDrw) { case 0: // Complete overlap -> delete the one under drwBuf = slices.Delete(drwBuf, i, i) case 1: // Partial overlap -> resize the one under drwBuf[i] = newDrw[0] default: drwBuf = slices.Delete(drwBuf, i, i) drwBuf = append(drwBuf, newDrw...) } } } } // Draw for _, clr := range clrBuf { if clr != nil { stack = append(stack, clr) } } for _, drw := range drwBuf { if drw != nil { stack = append(stack, drw) } } for _, inst := range stack { inst.Write(r.stream) } }