Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}