Transaction

A01BE28829595C…63773EC2CD12

Block 242,660 · index 1 · indexed

Summary

Hash
A01BE28829595CB7BE5BC58C71BB5E40972247A0A950C66A489863773EC2CD12
Block
242,660
Size
9467 bytes
Gas used
12,986,413 / 100,000,000
Fee
1000000 ugnot
Status
success

Messages

Attached funds
20000000 ugnot

Arguments · 11

  1. #1canvas
  2. #2README.md
  3. #3# canvas Pure package `gno.land/p/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/canvas/v0`: a fixed-size grid of palette indexes with lazy per-row allocation, plus the `x,y,color;...` batch codec. Used by the realm `gno.land/r/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/gnodraw/v0`, which adds pricing and access control. The package itself holds no chain state and has no chain imports, so it is fully covered by plain unit tests (`gno test .`). - `New(width, height)`: empty canvas, sides between 1 and 10000. - `Set(Pixel)`, `Get(x, y)`, `Check(Pixel)`: paint, read, validate. - `Rows(from, to)`: rows as one hex digit per pixel, no separators. - `ParsePixels`, `Encode`, `ParseColor`: the batch format. - `Palette`: 16 CSS colors, index 0 being "never painted".
  4. #4canvas.gno
  5. #5// Package canvas is a fixed-size, 16-color pixel grid. // // It holds no chain state and knows nothing about payments: the realm // gno.land/r/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/gnodraw/v0 wraps it with pricing and access control. // // Storage layout: one []byte per row, allocated on first paint. A row that // was never painted is nil and costs nothing. Each byte holds a palette // index; Blank (0) means "never painted". package canvas import ( "errors" "strconv" "strings" ) const ( // Blank is the color of a pixel nobody painted. It cannot be painted. Blank byte = 0 // MaxColor is the highest valid palette index. MaxColor byte = 15 // MaxSide bounds width and height so x*y fits comfortably in an int. MaxSide = 10000 ) // Palette maps a color index to its CSS hex value. Index 0 is Blank and is // rendered as white by clients. var Palette = [MaxColor + 1]string{ "#ffffff", // 0 blank "#ffffff", // 1 white "#e4e4e4", // 2 light gray "#888888", // 3 gray "#222222", // 4 black "#ffa7d1", // 5 pink "#e50000", // 6 red "#e59500", // 7 orange "#a06a42", // 8 brown "#e5d900", // 9 yellow "#94e044", // 10 lime "#02be01", // 11 green "#00d3dd", // 12 cyan "#0083c7", // 13 blue "#cf6ee4", // 14 magenta "#820080", // 15 purple } var ( ErrOutOfRange = errors.New("canvas: pixel out of range") ErrInvalidColor = errors.New("canvas: color must be between 1 and 15") ErrBadPixel = errors.New("canvas: pixel must be x,y,color") ) // Pixel is one paint request. type Pixel struct { X, Y int Color byte } // Canvas is a width x height grid of palette indexes. type Canvas struct { width, height int rows [][]byte painted int } // New returns an empty canvas. It panics on a non-positive or oversized side. func New(width, height int) *Canvas { if width <= 0 || height <= 0 || width > MaxSide || height > MaxSide { panic("canvas: width and height must be between 1 and " + strconv.Itoa(MaxSide)) } return &Canvas{ width: width, height: height, rows: make([][]byte, height), } } func (c *Canvas) Width() int { return c.width } func (c *Canvas) Height() int { return c.height } // Painted is the number of pixels that are not Blank. func (c *Canvas) Painted() int { return c.painted } // Check reports whether p can be painted on this canvas. func (c *Canvas) Check(p Pixel) error { if p.X < 0 || p.Y < 0 || p.X >= c.width || p.Y >= c.height { return ErrOutOfRange } if p.Color == Blank || p.Color > MaxColor { return ErrInvalidColor } return nil } // Get returns the color at (x, y). Out-of-range coordinates panic. func (c *Canvas) Get(x, y int) byte { if x < 0 || y < 0 || x >= c.width || y >= c.height { panic(ErrOutOfRange) } row := c.rows[y] if row == nil { return Blank } return row[x] } // Set paints p. It panics when Check(p) fails. func (c *Canvas) Set(p Pixel) { if err := c.Check(p); err != nil { panic(err) } row := c.rows[p.Y] if row == nil { row = make([]byte, c.width) c.rows[p.Y] = row } if row[p.X] == Blank { c.painted++ } row[p.X] = p.Color } // Rows encodes rows [from, to) as one hex digit per pixel, row after row, // with no separator. Clients slice it with the known width. func (c *Canvas) Rows(from, to int) string { if from < 0 || to > c.height || from > to { panic(ErrOutOfRange) } var sb strings.Builder sb.Grow((to - from) * c.width) for y := from; y < to; y++ { row := c.rows[y] if row == nil { for x := 0; x < c.width; x++ { sb.WriteByte('0') } continue } for _, color := range row { sb.WriteByte(hexDigit(color)) } } return sb.String() } // ParsePixels decodes "x,y,color;x,y,color;...". A trailing ';' is allowed. // It validates the syntax and the color range but not the coordinates, // which depend on the canvas: call Check for that. func ParsePixels(s string) ([]Pixel, error) { s = strings.TrimSuffix(s, ";") if s == "" { return nil, ErrBadPixel } parts := strings.Split(s, ";") pixels := make([]Pixel, 0, len(parts)) for _, part := range parts { fields := strings.Split(part, ",") if len(fields) != 3 { return nil, ErrBadPixel } x, err := strconv.Atoi(fields[0]) if err != nil { return nil, ErrBadPixel } y, err := strconv.Atoi(fields[1]) if err != nil { return nil, ErrBadPixel } color, err := ParseColor(fields[2]) if err != nil { return nil, err } pixels = append(pixels, Pixel{X: x, Y: y, Color: color}) } return pixels, nil } // ParseColor converts a decimal color index, rejecting Blank. func ParseColor(s string) (byte, error) { n, err := strconv.Atoi(s) if err != nil || n <= int(Blank) || n > int(MaxColor) { return 0, ErrInvalidColor } return byte(n), nil } // Encode is the inverse of ParsePixels. func Encode(pixels []Pixel) string { var sb strings.Builder for i, p := range pixels { if i > 0 { sb.WriteByte(';') } sb.WriteString(strconv.Itoa(p.X)) sb.WriteByte(',') sb.WriteString(strconv.Itoa(p.Y)) sb.WriteByte(',') sb.WriteString(strconv.Itoa(int(p.Color))) } return sb.String() } func hexDigit(v byte) byte { if v < 10 { return '0' + v } return 'a' + v - 10 }
  6. #6canvas_test.gno
  7. #7package canvas import ( "testing" "gno.land/p/nt/uassert/v0" ) func TestNewRejectsBadSizes(t *testing.T) { for _, tc := range []struct{ w, h int }{{0, 1}, {1, 0}, {-1, 1}, {MaxSide + 1, 1}, {1, MaxSide + 1}} { func() { defer func() { if r := recover(); r == nil { t.Errorf("New(%d, %d) did not panic", tc.w, tc.h) } }() New(tc.w, tc.h) }() } } func TestSetGet(t *testing.T) { c := New(4, 3) uassert.Equal(t, 4, c.Width()) uassert.Equal(t, 3, c.Height()) uassert.Equal(t, 0, c.Painted()) uassert.Equal(t, Blank, c.Get(3, 2)) c.Set(Pixel{X: 3, Y: 2, Color: 6}) uassert.Equal(t, byte(6), c.Get(3, 2)) uassert.Equal(t, 1, c.Painted()) // Repainting the same pixel does not count twice. c.Set(Pixel{X: 3, Y: 2, Color: 7}) uassert.Equal(t, byte(7), c.Get(3, 2)) uassert.Equal(t, 1, c.Painted()) // Other rows stay unallocated and blank. uassert.Equal(t, Blank, c.Get(0, 0)) uassert.True(t, c.rows[0] == nil, "row 0 should stay nil") } func TestCheck(t *testing.T) { c := New(2, 2) tests := []struct { name string p Pixel err error }{ {"ok", Pixel{0, 0, 1}, nil}, {"ok max", Pixel{1, 1, MaxColor}, nil}, {"x negative", Pixel{-1, 0, 1}, ErrOutOfRange}, {"y negative", Pixel{0, -1, 1}, ErrOutOfRange}, {"x too big", Pixel{2, 0, 1}, ErrOutOfRange}, {"y too big", Pixel{0, 2, 1}, ErrOutOfRange}, {"blank color", Pixel{0, 0, Blank}, ErrInvalidColor}, {"color too big", Pixel{0, 0, MaxColor + 1}, ErrInvalidColor}, } for _, tc := range tests { got := c.Check(tc.p) if got != tc.err { t.Errorf("%s: got %v, want %v", tc.name, got, tc.err) } } } func TestRows(t *testing.T) { c := New(3, 3) uassert.Equal(t, "000000000", c.Rows(0, 3)) c.Set(Pixel{X: 1, Y: 1, Color: 15}) c.Set(Pixel{X: 2, Y: 2, Color: 10}) uassert.Equal(t, "000", c.Rows(0, 1)) uassert.Equal(t, "0f0", c.Rows(1, 2)) uassert.Equal(t, "0f000a", c.Rows(1, 3)) uassert.Equal(t, "", c.Rows(2, 2)) for _, r := range [][2]int{{-1, 1}, {0, 4}, {2, 1}} { func() { defer func() { if rec := recover(); rec == nil { t.Errorf("Rows(%d, %d) did not panic", r[0], r[1]) } }() c.Rows(r[0], r[1]) }() } } func TestParsePixels(t *testing.T) { px, err := ParsePixels("0,0,1;5,7,15;") uassert.NoError(t, err) uassert.Equal(t, 2, len(px)) if px[0] != (Pixel{0, 0, 1}) || px[1] != (Pixel{5, 7, 15}) { t.Errorf("unexpected pixels: %v", px) } for _, bad := range []string{"", ";", "1,2", "1,2,3,4", "a,2,3", "1,b,3", "1,2,c", "1,2,0", "1,2,16", "1,2,-1"} { if _, err := ParsePixels(bad); err == nil { t.Errorf("ParsePixels(%q) accepted invalid input", bad) } } } func TestEncodeRoundTrip(t *testing.T) { in := []Pixel{{0, 0, 1}, {999, 999, 15}, {3, 4, 8}} s := Encode(in) uassert.Equal(t, "0,0,1;999,999,15;3,4,8", s) out, err := ParsePixels(s) uassert.NoError(t, err) uassert.Equal(t, len(in), len(out)) for i := range in { if in[i] != out[i] { t.Errorf("pixel %d: got %v, want %v", i, out[i], in[i]) } } }
  8. #8gnomod.toml
  9. #9module = "gno.land/p/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/canvas/v0" gno = "0.9"
  10. #10/gno.MemPackageType
  11. #11 MPUserAll

Result log

msg:0,success:true,log:,events:[]