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

canvas.gno

5.04 Kb · 208 lines
  1// Package canvas is a fixed-size, 16-color pixel grid.
  2//
  3// It holds no chain state and knows nothing about payments: the realm
  4// gno.land/r/g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr/million/v0 wraps it with pricing and access control.
  5//
  6// Storage layout: one []byte per row, allocated on first paint. A row that
  7// was never painted is nil and costs nothing. Each byte holds a palette
  8// index; Blank (0) means "never painted".
  9package canvas
 10
 11import (
 12	"errors"
 13	"strconv"
 14	"strings"
 15)
 16
 17const (
 18	// Blank is the color of a pixel nobody painted. It cannot be painted.
 19	Blank byte = 0
 20	// MaxColor is the highest valid palette index.
 21	MaxColor byte = 15
 22	// MaxSide bounds width and height so x*y fits comfortably in an int.
 23	MaxSide = 10000
 24)
 25
 26// Palette maps a color index to its CSS hex value. Index 0 is Blank and is
 27// rendered as white by clients.
 28var Palette = [MaxColor + 1]string{
 29	"#ffffff", // 0 blank
 30	"#ffffff", // 1 white
 31	"#e4e4e4", // 2 light gray
 32	"#888888", // 3 gray
 33	"#222222", // 4 black
 34	"#ffa7d1", // 5 pink
 35	"#e50000", // 6 red
 36	"#e59500", // 7 orange
 37	"#a06a42", // 8 brown
 38	"#e5d900", // 9 yellow
 39	"#94e044", // 10 lime
 40	"#02be01", // 11 green
 41	"#00d3dd", // 12 cyan
 42	"#0083c7", // 13 blue
 43	"#cf6ee4", // 14 magenta
 44	"#820080", // 15 purple
 45}
 46
 47var (
 48	ErrOutOfRange   = errors.New("canvas: pixel out of range")
 49	ErrInvalidColor = errors.New("canvas: color must be between 1 and 15")
 50	ErrBadPixel     = errors.New("canvas: pixel must be x,y,color")
 51)
 52
 53// Pixel is one paint request.
 54type Pixel struct {
 55	X, Y  int
 56	Color byte
 57}
 58
 59// Canvas is a width x height grid of palette indexes.
 60type Canvas struct {
 61	width, height int
 62	rows          [][]byte
 63	painted       int
 64}
 65
 66// New returns an empty canvas. It panics on a non-positive or oversized side.
 67func New(width, height int) *Canvas {
 68	if width <= 0 || height <= 0 || width > MaxSide || height > MaxSide {
 69		panic("canvas: width and height must be between 1 and " + strconv.Itoa(MaxSide))
 70	}
 71	return &Canvas{
 72		width:  width,
 73		height: height,
 74		rows:   make([][]byte, height),
 75	}
 76}
 77
 78func (c *Canvas) Width() int  { return c.width }
 79func (c *Canvas) Height() int { return c.height }
 80
 81// Painted is the number of pixels that are not Blank.
 82func (c *Canvas) Painted() int { return c.painted }
 83
 84// Check reports whether p can be painted on this canvas.
 85func (c *Canvas) Check(p Pixel) error {
 86	if p.X < 0 || p.Y < 0 || p.X >= c.width || p.Y >= c.height {
 87		return ErrOutOfRange
 88	}
 89	if p.Color == Blank || p.Color > MaxColor {
 90		return ErrInvalidColor
 91	}
 92	return nil
 93}
 94
 95// Get returns the color at (x, y). Out-of-range coordinates panic.
 96func (c *Canvas) Get(x, y int) byte {
 97	if x < 0 || y < 0 || x >= c.width || y >= c.height {
 98		panic(ErrOutOfRange)
 99	}
100	row := c.rows[y]
101	if row == nil {
102		return Blank
103	}
104	return row[x]
105}
106
107// Set paints p. It panics when Check(p) fails.
108func (c *Canvas) Set(p Pixel) {
109	if err := c.Check(p); err != nil {
110		panic(err)
111	}
112	row := c.rows[p.Y]
113	if row == nil {
114		row = make([]byte, c.width)
115		c.rows[p.Y] = row
116	}
117	if row[p.X] == Blank {
118		c.painted++
119	}
120	row[p.X] = p.Color
121}
122
123// Rows encodes rows [from, to) as one hex digit per pixel, row after row,
124// with no separator. Clients slice it with the known width.
125func (c *Canvas) Rows(from, to int) string {
126	if from < 0 || to > c.height || from > to {
127		panic(ErrOutOfRange)
128	}
129	var sb strings.Builder
130	sb.Grow((to - from) * c.width)
131	for y := from; y < to; y++ {
132		row := c.rows[y]
133		if row == nil {
134			for x := 0; x < c.width; x++ {
135				sb.WriteByte('0')
136			}
137			continue
138		}
139		for _, color := range row {
140			sb.WriteByte(hexDigit(color))
141		}
142	}
143	return sb.String()
144}
145
146// ParsePixels decodes "x,y,color;x,y,color;...". A trailing ';' is allowed.
147// It validates the syntax and the color range but not the coordinates,
148// which depend on the canvas: call Check for that.
149func ParsePixels(s string) ([]Pixel, error) {
150	s = strings.TrimSuffix(s, ";")
151	if s == "" {
152		return nil, ErrBadPixel
153	}
154	parts := strings.Split(s, ";")
155	pixels := make([]Pixel, 0, len(parts))
156	for _, part := range parts {
157		fields := strings.Split(part, ",")
158		if len(fields) != 3 {
159			return nil, ErrBadPixel
160		}
161		x, err := strconv.Atoi(fields[0])
162		if err != nil {
163			return nil, ErrBadPixel
164		}
165		y, err := strconv.Atoi(fields[1])
166		if err != nil {
167			return nil, ErrBadPixel
168		}
169		color, err := ParseColor(fields[2])
170		if err != nil {
171			return nil, err
172		}
173		pixels = append(pixels, Pixel{X: x, Y: y, Color: color})
174	}
175	return pixels, nil
176}
177
178// ParseColor converts a decimal color index, rejecting Blank.
179func ParseColor(s string) (byte, error) {
180	n, err := strconv.Atoi(s)
181	if err != nil || n <= int(Blank) || n > int(MaxColor) {
182		return 0, ErrInvalidColor
183	}
184	return byte(n), nil
185}
186
187// Encode is the inverse of ParsePixels.
188func Encode(pixels []Pixel) string {
189	var sb strings.Builder
190	for i, p := range pixels {
191		if i > 0 {
192			sb.WriteByte(';')
193		}
194		sb.WriteString(strconv.Itoa(p.X))
195		sb.WriteByte(',')
196		sb.WriteString(strconv.Itoa(p.Y))
197		sb.WriteByte(',')
198		sb.WriteString(strconv.Itoa(int(p.Color)))
199	}
200	return sb.String()
201}
202
203func hexDigit(v byte) byte {
204	if v < 10 {
205		return '0' + v
206	}
207	return 'a' + v - 10
208}