1// Package canvas is a fixed-size, 16-color pixel grid.2//3// It holds no chain state and knows nothing about payments: the realm4// gno.land/r/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/gnodraw/v0 wraps it with pricing and access control.5//6// Storage layout: one []byte per row, allocated on first paint. A row that7// was never painted is nil and costs nothing. Each byte holds a palette8// index; Blank (0) means "never painted".9package canvas1011import (12 "errors"13 "strconv"14 "strings"15)1617const (18 // Blank is the color of a pixel nobody painted. It cannot be painted.19 Blank byte = 020 // MaxColor is the highest valid palette index.21 MaxColor byte = 1522 // MaxSide bounds width and height so x*y fits comfortably in an int.23 MaxSide = 1000024)2526// Palette maps a color index to its CSS hex value. Index 0 is Blank and is27// rendered as white by clients.28var Palette = [MaxColor + 1]string{29 "#ffffff", // 0 blank30 "#ffffff", // 1 white31 "#e4e4e4", // 2 light gray32 "#888888", // 3 gray33 "#222222", // 4 black34 "#ffa7d1", // 5 pink35 "#e50000", // 6 red36 "#e59500", // 7 orange37 "#a06a42", // 8 brown38 "#e5d900", // 9 yellow39 "#94e044", // 10 lime40 "#02be01", // 11 green41 "#00d3dd", // 12 cyan42 "#0083c7", // 13 blue43 "#cf6ee4", // 14 magenta44 "#820080", // 15 purple45}4647var (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)5253// Pixel is one paint request.54type Pixel struct {55 X, Y int56 Color byte57}5859// Canvas is a width x height grid of palette indexes.60type Canvas struct {61 width, height int62 rows [][]byte63 painted int64}6566// 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}7778func (c *Canvas) Width() int { return c.width }79func (c *Canvas) Height() int { return c.height }8081// Painted is the number of pixels that are not Blank.82func (c *Canvas) Painted() int { return c.painted }8384// 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 ErrOutOfRange88 }89 if p.Color == Blank || p.Color > MaxColor {90 return ErrInvalidColor91 }92 return nil93}9495// 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 Blank103 }104 return row[x]105}106107// 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] = row116 }117 if row[p.X] == Blank {118 c.painted++119 }120 row[p.X] = p.Color121}122123// 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.Builder130 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 continue138 }139 for _, color := range row {140 sb.WriteByte(hexDigit(color))141 }142 }143 return sb.String()144}145146// 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, ErrBadPixel153 }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, ErrBadPixel160 }161 x, err := strconv.Atoi(fields[0])162 if err != nil {163 return nil, ErrBadPixel164 }165 y, err := strconv.Atoi(fields[1])166 if err != nil {167 return nil, ErrBadPixel168 }169 color, err := ParseColor(fields[2])170 if err != nil {171 return nil, err172 }173 pixels = append(pixels, Pixel{X: x, Y: y, Color: color})174 }175 return pixels, nil176}177178// 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, ErrInvalidColor183 }184 return byte(n), nil185}186187// Encode is the inverse of ParsePixels.188func Encode(pixels []Pixel) string {189 var sb strings.Builder190 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}202203func hexDigit(v byte) byte {204 if v < 10 {205 return '0' + v206 }207 return 'a' + v - 10208}209Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.