canvas.gno
5.71 Kb · 233 lines
1package art
2
3import "strings"
4
5// Cell is one character cell: a rune and its two colours. FG and BG are packed
6// 0xRRGGBB values, or [Default].
7type Cell struct {
8 R rune
9 FG, BG int
10}
11
12// Canvas is a fixed grid of cells, addressed (x, y) from the top left.
13//
14// FG and BG are the pen: the colours the drawing helpers ([Write], [HLine],
15// [Box] and the rest) apply to every cell they touch. They start at [Default].
16// Set them with [Canvas.Pen]; a caller that only wants shapes can ignore them
17// entirely.
18type Canvas struct {
19 W, H int
20 Cells []Cell
21 FG, BG int
22}
23
24// Blank is the cell a new canvas is filled with.
25var Blank = Cell{R: ' ', FG: Default, BG: Default}
26
27// NewCanvas returns a w by h canvas of blank cells. Negative dimensions are
28// treated as zero, so a canvas built from arithmetic that underflowed is empty
29// rather than a panic halfway through a Render.
30func NewCanvas(w, h int) *Canvas {
31 if w < 0 {
32 w = 0
33 }
34 if h < 0 {
35 h = 0
36 }
37 cells := make([]Cell, w*h)
38 for i := 0; i < len(cells); i++ {
39 cells[i] = Blank
40 }
41 return &Canvas{W: w, H: h, Cells: cells, FG: Default, BG: Default}
42}
43
44// Pen sets the colours the drawing helpers will apply, and returns the canvas
45// so calls can be chained.
46func (c *Canvas) Pen(fg, bg int) *Canvas {
47 c.FG, c.BG = fg, bg
48 return c
49}
50
51// In reports whether (x, y) is on the canvas. Every helper in this package
52// clips silently rather than panicking: art is drawn by arithmetic and a
53// one-off-the-edge line is not worth aborting a Render over.
54func (c *Canvas) In(x, y int) bool {
55 return x >= 0 && y >= 0 && x < c.W && y < c.H
56}
57
58// At returns the cell at (x, y), or [Blank] if that is off the canvas.
59func (c *Canvas) At(x, y int) Cell {
60 if !c.In(x, y) {
61 return Blank
62 }
63 return c.Cells[y*c.W+x]
64}
65
66// Set writes a whole cell, colours included, ignoring the pen.
67func (c *Canvas) Set(x, y int, cell Cell) {
68 if !c.In(x, y) {
69 return
70 }
71 c.Cells[y*c.W+x] = cell
72}
73
74// Put writes one rune in the pen's colours.
75func (c *Canvas) Put(x, y int, r rune) {
76 c.Set(x, y, Cell{R: r, FG: c.FG, BG: c.BG})
77}
78
79// Fill sets every cell on the canvas to cell.
80func (c *Canvas) Fill(cell Cell) {
81 for i := 0; i < len(c.Cells); i++ {
82 c.Cells[i] = cell
83 }
84}
85
86// Write draws s left to right from (x, y) in the pen's colours. It does not
87// wrap: anything past the right edge is clipped. A '\n' moves to the next row,
88// back at the starting column, which is what makes a multi-line literal blit
89// the way it reads in source.
90func (c *Canvas) Write(x, y int, s string) {
91 cx, cy := x, y
92 for _, r := range s {
93 if r == '\n' {
94 cx, cy = x, cy+1
95 continue
96 }
97 c.Put(cx, cy, r)
98 cx++
99 }
100}
101
102// HLine draws n runes rightwards from (x, y).
103func (c *Canvas) HLine(x, y, n int, r rune) {
104 for i := 0; i < n; i++ {
105 c.Put(x+i, y, r)
106 }
107}
108
109// VLine draws n runes downwards from (x, y).
110func (c *Canvas) VLine(x, y, n int, r rune) {
111 for i := 0; i < n; i++ {
112 c.Put(x, y+i, r)
113 }
114}
115
116// Rect fills a w by h area with r.
117func (c *Canvas) Rect(x, y, w, h int, r rune) {
118 for j := 0; j < h; j++ {
119 c.HLine(x, y+j, w, r)
120 }
121}
122
123// Blit copies src onto c with src's top left at (x, y), colours and all.
124// Cells landing off the edge are dropped.
125func (c *Canvas) Blit(x, y int, src *Canvas) {
126 if src == nil {
127 return
128 }
129 for j := 0; j < src.H; j++ {
130 for i := 0; i < src.W; i++ {
131 c.Set(x+i, y+j, src.Cells[j*src.W+i])
132 }
133 }
134}
135
136// Text emits the canvas as plain text: runes only, one line per row, trailing
137// spaces trimmed. This is the form that belongs inside a triple-backtick fence,
138// and the form a caller can pipe.
139//
140// Colours are dropped, which is the whole point: a consumer asking for text has
141// said it cannot show them.
142func (c Canvas) Text() string {
143 var b strings.Builder
144 for y := 0; y < c.H; y++ {
145 var line strings.Builder
146 for x := 0; x < c.W; x++ {
147 line.WriteRune(c.Cells[y*c.W+x].R)
148 }
149 b.WriteString(strings.TrimRight(line.String(), " "))
150 if y < c.H-1 {
151 b.WriteString("\n")
152 }
153 }
154 return b.String()
155}
156
157// String is [Canvas.Text], so a canvas can be printed directly.
158func (c Canvas) String() string { return c.Text() }
159
160// ANSI emits the canvas with SGR truecolour escapes, one reset at the end of
161// every coloured row. Trailing spaces are NOT trimmed here: a trailing cell may
162// carry a background colour, and trimming it would put a hole in the picture.
163//
164// This output cannot reach gnoweb. See the package doc.
165func (c Canvas) ANSI() string {
166 var b strings.Builder
167 for y := 0; y < c.H; y++ {
168 fg, bg := Default, Default
169 for x := 0; x < c.W; x++ {
170 cl := c.Cells[y*c.W+x]
171 if cl.FG != fg || cl.BG != bg {
172 b.WriteString(sgr(cl.FG, cl.BG))
173 fg, bg = cl.FG, cl.BG
174 }
175 b.WriteRune(cl.R)
176 }
177 if fg != Default || bg != Default {
178 b.WriteString(reset)
179 }
180 if y < c.H-1 {
181 b.WriteString("\n")
182 }
183 }
184 return b.String()
185}
186
187// Escape is the byte that starts every SGR sequence. Exported so a consumer can
188// strip ANSI without hard-coding "\x1b".
189const Escape = '\x1b'
190
191const reset = "\x1b[0m"
192
193func sgr(fg, bg int) string {
194 var b strings.Builder
195 b.WriteString("\x1b[")
196 if fg == Default {
197 b.WriteString("39")
198 } else {
199 r, g, bl := Split(fg)
200 b.WriteString("38;2;")
201 writeInt(&b, r)
202 b.WriteByte(';')
203 writeInt(&b, g)
204 b.WriteByte(';')
205 writeInt(&b, bl)
206 }
207 b.WriteByte(';')
208 if bg == Default {
209 b.WriteString("49")
210 } else {
211 r, g, bl := Split(bg)
212 b.WriteString("48;2;")
213 writeInt(&b, r)
214 b.WriteByte(';')
215 writeInt(&b, g)
216 b.WriteByte(';')
217 writeInt(&b, bl)
218 }
219 b.WriteByte('m')
220 return b.String()
221}
222
223// writeInt writes a 0-255 value without strconv, which this package would
224// otherwise import for three call sites.
225func writeInt(b *strings.Builder, v int) {
226 if v >= 100 {
227 b.WriteByte(byte('0' + v/100))
228 }
229 if v >= 10 {
230 b.WriteByte(byte('0' + (v/10)%10))
231 }
232 b.WriteByte(byte('0' + v%10))
233}