summaryrefslogtreecommitdiff
path: root/internal/render/renderer.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/render/renderer.go')
-rw-r--r--internal/render/renderer.go167
1 files changed, 167 insertions, 0 deletions
diff --git a/internal/render/renderer.go b/internal/render/renderer.go
new file mode 100644
index 0000000..c527413
--- /dev/null
+++ b/internal/render/renderer.go
@@ -0,0 +1,167 @@
+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) Start() {
+ 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)
+ // 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 != None {
+ 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 != None {
+ 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 {
+ clr.Write(r.stream)
+ }
+ for _, drw := range drwBuf {
+ drw.Write(r.stream)
+ }
+}