summaryrefslogtreecommitdiff
path: root/internal/render/renderer.go
blob: 5d3cb87d2b33493824bb87d7e623181b1b8f8b8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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)
	// 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)
	}
}