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}