Transaction
5C2EDD8BA80234…4E6753E24659
Block 299,639 · index 0 · indexed
Summary
- Hash
- 5C2EDD8BA802347BE90077F541CD80190FF682624FDBF471F9EB4E6753E24659
- Block
- 299,639
- Size
- 21845 bytes
- Gas used
- 28,321,863 / 250,000,000
- Fee
- 250000 ugnot
- Status
- success
Messages
Arguments · 17
- #1nft
- #2README.md
- #3# Settlers NTF Package that draws the Settlers, small pixel-art gnomes, as an SVG. Given the same traits, it always draws the same image, so it's safe and cheap for any realm to import and call directly. Code also lives in [jeronimoalbi/gnoland-settlers](https://github.com/jeronimoalbi/gnoland-settlers). ## From a seed ```go traits := nft.NewTraits("some-seed-string") svg := nft.RenderSVG(traits, nft.Options{Size: 512}) ``` The traits (skin, hat, beard, clothes, glasses, background, and so on) are all worked out deterministically from the seed, so the same seed always gives the same gnome. ## From packed traits Traits are usually stored packed into a single `uint32`: ```go packed := nft.Pack(traits) traits, err := nft.FromPacked(packed) svg := nft.RenderSVG(traits, nft.Options{}) ``` `Options` also takes `NoBackground` (a transparent image) and `BackgroundColor` (a custom one). `RenderDataURI` gives the same image as a `data:image/svg+xml;...` URI, ready to use as an `<img>` source. ## License The art itself is dedicated to the public domain under CC0 1.0 Universal; see `Credits`.
- #4data.gno
- #5package nft // Parts. Every row is the left half of the sprite, left-padded with '.' (transparent) to 12 // columns, and parts are the rows one after the other. Row i starts at i*12. const hatTall = "" + "...........o" + // y=0 "..........oh" + // y=1 "..........oH" + // y=2 ".........ohH" + // y=3 ".........oHH" + // y=4 "........ohHH" + // y=5 "........oHHH" + // y=6 ".......ohHHH" + // y=7 ".......oHHHH" + // y=8 "......ohHHHH" + // y=9 "......oHHHHH" // y=10 const brim = "" + "....ohHHHHHH" + // y=11 "....ojjjjjjj" // y=12 const head = "" + "......ouuuuu" + // y=13 "......osWWWs" + // y=14 ".....ossesss" + // y=15 "....oussessn" + // y=16 "......osssmn" // y=17 const headBeard = "" + ".....owWWWvv" // y=18 const headNoBeard = "" + "......osssvv" + // y=18 "......osssuu" + // y=19 ".......ossss" + // y=20 "........oooo" // y=21 const beardLong = "" + ".....owWWWWW" + // y=19 ".....owWWWxW" + // y=20 "......owWWxW" + // y=21 "......owWxWW" + // y=22 ".......owWxW" + // y=23 ".......owWWW" + // y=24 "........owWx" + // y=25 ".........owW" + // y=26 "..........ow" // y=27 const beardPointy = "" + ".....owWWWWW" + // y=19 "......owWWWW" + // y=20 "......owWWxW" + // y=21 ".......owWWW" + // y=22 ".......owWxW" + // y=23 "........owWW" + // y=24 "........owWx" + // y=25 ".........owW" + // y=26 ".........owx" + // y=27 "..........ow" // y=28 const beardRound = "" + ".....owWWWWW" + // y=19 ".....owWWWWW" + // y=20 "......owWWWW" + // y=21 ".......owWWx" + // y=22 "........owWW" + // y=23 ".........owW" + // y=24 "..........ow" // y=25 const body = "" + "....ocCCCCCC" + // y=19 "...ocCCCCCCC" + // y=20 "...ocCCCCCCC" + // y=21 "...ocCCCCCCC" + // y=22 "..oscCCCCCCC" + // y=23 "..oucCCCCCCC" + // y=24 "...okkkkkkky" + // y=25 "...ocCCCCCCC" + // y=26 "...odddddddd" // y=27 const boots = "" + ".....opppppo" + // y=28 ".....opppppo" + // y=29 ".....oqqqqqo" + // y=30 ".....ooooooo" // y=31 // Bent-tip hat, see the spec section 6.1. const coneY0 = 2 var coneK = [...]int{1, 1, 2, 2, 3, 3, 4, 4, 5} // tailSpans are the pixels of the tip, when it falls to the right: row y, from x0 to x1 (inclusive). var tailSpans = [...]span{{1, 11, 14}, {2, 14, 15}, {3, 15, 15}} // Glasses, see the spec section 6.2. Pairs are x, y. var ( glassesFrame = [...][2]int{{7, 15}, {8, 15}, {9, 15}, {14, 15}, {15, 15}, {16, 15}, {7, 16}, {9, 16}, {10, 16}, {13, 16}, {14, 16}, {16, 16}, {7, 17}, {8, 17}, {9, 17}, {14, 17}, {15, 17}, {16, 17}} glassesEyes = [...][2]int{{8, 16}, {15, 16}} ) // Palettes, as hex digits without '#'. Every entry has 3 tones: skin t s u, hat h H j, // beard w W x, tunic c C d. Entry i starts at i*3. var ( skinColors = [...]string{"ffd2b0", "f2b58c", "d18f68", "f0bd94", "d99a6c", "b57650", "c98f66", "a86f48", "83502f"} hatColors = [...]string{"f06a5a", "d1413a", "9c2a33", "6aa0f0", "3f6fd1", "2b4a99", "7ed36a", "4aa84a", "2e7a3a", "b58cf0", "8a5cd1", "5e3c9a", "ffb45a", "f08a2a", "b5601c"} beardColors = [...]string{"ffffff", "e9ecf2", "b9c0d0", "d5d9e0", "aab1bd", "7b8494", "ffb05a", "e07a2a", "a4501c", "a8744e", "7f5234", "573622"} tunicColors = [...]string{"5fb0e8", "3f8fd1", "2b649a", "8fd06a", "5fa83f", "3d7a2a", "c98a5a", "a4673a", "75452a", "e9868f", "cc5a68", "994050", "f0d36a", "d1a83a", "a07a25"} glassesColors = [...]string{"2a1a2e", "e8b422", "d1413a", "2aa198"} bgColors = [...]string{"cfe8f3", "f3e3c9", "dbeed2", "eadcf3", "f6d9d5"} ) // fixedColor returns the color of the keys that do not depend on a trait, or "". func fixedColor(key byte) string { switch key { case 'o': return "2a1a2e" case 'e': return "1b1626" case 'k': return "6b3f22" case 'y': return "f5c542" case 'p': return "6a4126" case 'q': return "46271a" case 'n': return "f08c7c" case 'm': return "ffb0a0" case 'v': return "c8645a" } return "" } // swapTable has the key used on the right half for each key of the left half. var swapTable = [128]byte{ 'C': 'C', 'H': 'H', 'W': 'W', 'c': 'd', 'd': 'd', 'e': 'e', 'h': 'j', 'j': 'j', 'k': 'k', 'm': 'v', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 's': 's', 'u': 'u', 'v': 'v', 'w': 'x', 'x': 'x', 'y': 'y', } // Path segments are "M{x} {y}h{n}v1h-{n}z", written as mHead[x] + nums[y] + runTail[n]. // The percent-encoded variant only differs in the space, that is written as %20. var ( rawMHead = [...]string{"M0 ", "M1 ", "M2 ", "M3 ", "M4 ", "M5 ", "M6 ", "M7 ", "M8 ", "M9 ", "M10 ", "M11 ", "M12 ", "M13 ", "M14 ", "M15 ", "M16 ", "M17 ", "M18 ", "M19 ", "M20 ", "M21 ", "M22 ", "M23 "} uriMHead = [...]string{"M0%20", "M1%20", "M2%20", "M3%20", "M4%20", "M5%20", "M6%20", "M7%20", "M8%20", "M9%20", "M10%20", "M11%20", "M12%20", "M13%20", "M14%20", "M15%20", "M16%20", "M17%20", "M18%20", "M19%20", "M20%20", "M21%20", "M22%20", "M23%20"} nums = [...]string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31"} runTail = [...]string{"", "h1v1h-1z", "h2v1h-2z", "h3v1h-3z", "h4v1h-4z", "h5v1h-5z", "h6v1h-6z", "h7v1h-7z", "h8v1h-8z", "h9v1h-9z", "h10v1h-10z", "h11v1h-11z", "h12v1h-12z", "h13v1h-13z", "h14v1h-14z", "h15v1h-15z", "h16v1h-16z", "h17v1h-17z", "h18v1h-18z", "h19v1h-19z", "h20v1h-20z", "h21v1h-21z", "h22v1h-22z", "h23v1h-23z", "h24v1h-24z"} )
- #6doc.gno
- #7// Package `nft` draws the Settlers, pixel-art gnomes. // The art is dedicated to the public domain under CC0 1.0, see Credits. package nft const ( // Style is the ID of the art style. Style = "settlers-v1" // Credits is the license of the art. Credits = "The art of the Settlers is dedicated to the public domain under " + "[CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/)" )
- #8gnomod.toml
- #9module = "gno.land/p/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft/v0" gno = "0.9"
- #10grid.gno
- #11package nft const ( gridW = 24 gridH = 32 half = 12 // columns of the left half, where rows are written ) // span is a horizontal range of pixels, x0 to x1 inclusive. type span struct{ y, x0, x1 int } // BuildGrid returns the pixel grid of a gnome, as 32 rows of 24 palette keys ('.' is transparent). // Traits must be valid. func BuildGrid(t Traits) []string { g := buildGrid(t) rows := make([]string, gridH) for y := 0; y < gridH; y++ { row := make([]byte, gridW) for x := 0; x < gridW; x++ { c := g[y*gridW+x] if c == 0 { c = '.' } row[x] = c } rows[y] = string(row) } return rows } // buildGrid draws all the layers of a gnome. The grid has a byte per pixel, row by row, and 0 is transparent. func buildGrid(t Traits) []byte { if err := t.Validate(); err != nil { panic("settlers: " + err.Error()) } g := make([]byte, gridW*gridH) draw(g, body, 19) if t.Beard == 0 { draw(g, head+headNoBeard, 13) } else { switch t.Beard { case 1: draw(g, beardLong, 19) case 2: draw(g, beardPointy, 19) default: draw(g, beardRound, 19) } draw(g, head+headBeard, 13) } if t.Glasses > 0 { for _, p := range glassesFrame { put(g, p[0], p[1], 'g') } for _, p := range glassesEyes { put(g, p[0], p[1], 'e') } } if t.HatShape == 0 { draw(g, hatTall, 0) } else { drawBentHat(g, t.Flip == 1) } draw(g, brim, 11) draw(g, boots, 28) return g } // draw draws rows, that are the left half of the sprite, starting at row y0. The right half is // the left half mirrored, where light tones are swapped by their shaded twin. func draw(g []byte, rows string, y0 int) { for i := 0; i*half < len(rows); i++ { y := y0 + i if y < 0 || y >= gridH { continue } row := rows[i*half : (i+1)*half] for j := 0; j < half; j++ { c := row[j] if c != '.' { g[y*gridW+j] = c g[y*gridW+gridW-1-j] = swapTable[c] } } } } func put(g []byte, x, y int, c byte) { if x >= 0 && x < gridW && y >= 0 && y < gridH { g[y*gridW+x] = c } } // drawBentHat draws the hat with the tip that falls to the right, or to the left when flipped. // // It is written without function calls (no closures or helpers), because they are the most // expensive thing in Gno. The fill has a border of one cell, so neighbors never need a bounds check. func drawBentHat(g []byte, flip bool) { const ( rows = coneY0 + len(coneK) + 1 // rows of the hat, one more than the cone for its outline stride = gridW + 2 ) var fill [(rows + 2) * stride]bool // No maps are used to keep it deterministic for i, k := range coneK { for x := half - k; x < half+k; x++ { if flip { fill[(coneY0+i+1)*stride+gridW-x] = true // mirrored column gridW-1-x, plus the border } else { fill[(coneY0+i+1)*stride+x+1] = true } } } for _, s := range tailSpans { for x := s.x0; x <= s.x1; x++ { if flip { fill[(s.y+1)*stride+gridW-x] = true } else { fill[(s.y+1)*stride+x+1] = true } } } for y := 0; y < rows; y++ { for x := 0; x < gridW; x++ { i := (y+1)*stride + x + 1 if !fill[i] { continue } left, right, up, down := fill[i-1], fill[i+1], fill[i-stride], fill[i+stride] // Shading c := byte('H') if !left { if y%2 == 1 { c = 'h' } } else if !right || !down { c = 'j' } g[y*gridW+x] = c // Outline. It only touches pixels that are not part of the hat, so it doesn't matter // that it is done in the same pass as the shading. if !left && x > 0 { g[y*gridW+x-1] = 'o' } if !right && x < gridW-1 { g[y*gridW+x+1] = 'o' } if !up && y > 0 { g[(y-1)*gridW+x] = 'o' } if !down { g[(y+1)*gridW+x] = 'o' } } } }
- #12svg.gno
- #13package nft import ( "encoding/base64" "strconv" ) // CanonicalSize is the width and height in pixels of the image used for NFTs. const CanonicalSize = 512 // Options are the options to render a gnome. type Options struct { // Size is the width and the height of the image in pixels, because the image is a square. // Multiples of 32 keep every art pixel as a whole number of screen pixels. // Zero omits the width and height, so that the image scales to its container. Size int // NoBackground omits the background. NoBackground bool // BackgroundColor replaces the color of the background, in "#rrggbb" format. BackgroundColor string } // RenderSVG renders the gnome as an SVG document. // Traits must be valid. // The same traits and options always give the same bytes. func RenderSVG(t Traits, o Options) string { return render(t, o, &rawSyntax) } // RenderSeed renders the gnome of a seed as an SVG document. func RenderSeed(seed string, o Options) string { return RenderSVG(NewTraits(seed), o) } // RenderDataURI renders the gnome as a "data:image/svg+xml;charset=utf-8," URI, percent-encoded. // // Cheapest way to embed a gnome. // SVG is written encoded, instead of encoding it afterwards. // Traits must be valid. func RenderDataURI(t Traits, o Options) string { return "data:image/svg+xml;charset=utf-8," + render(t, o, &uriSyntax) } // DataURI encodes an SVG document as a "data:image/svg+xml;base64," URI. // It is slower than RenderDataURI, use it when Base64 is needed. func DataURI(svg string) string { return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg)) } // syntax define the fixed pieces of the SVG document, that are the same for every gnome. type syntax struct { open string // start of the svg element, up to its size widthOpen string heightOpen string sizeClose string openEnd string // end of the start of the svg element rectOpen string // background, before its color rectClose string pathOpen string // before the color pathMid string // after the color, before the segments pathClose string end string mHead []string // start of a segment, by x } // rawSyntax contains the fixed pieces of the SVG document. var rawSyntax = syntax{ open: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 0 32 32"`, widthOpen: ` width="`, heightOpen: `" height="`, sizeClose: `"`, openEnd: ` shape-rendering="crispEdges">`, rectOpen: `<rect x="-4" width="32" height="32" fill="#`, rectClose: `"/>`, pathOpen: `<path fill="#`, pathMid: `" d="`, pathClose: `"/>`, end: `</svg>`, mHead: rawMHead[:], } // uriSyntax contains the fixes pieces of the SVG document encoded for data URI. var uriSyntax = syntax{ open: `%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%22-4%200%2032%2032%22`, widthOpen: `%20width=%22`, heightOpen: `%22%20height=%22`, sizeClose: `%22`, openEnd: `%20shape-rendering=%22crispEdges%22%3E`, rectOpen: `%3Crect%20x=%22-4%22%20width=%2232%22%20height=%2232%22%20fill=%22%23`, rectClose: `%22/%3E`, pathOpen: `%3Cpath%20fill=%22%23`, pathMid: `%22%20d=%22`, pathClose: `%22/%3E`, end: `%3C/svg%3E`, mHead: uriMHead[:], } // maxGroups is the biggest number of different colors of a gnome: 22 keys. const maxGroups = 24 // render draws the gnome: // - Pixels are scanned row by row, and horizontal runs of the same key are one segment. // - Segments are grouped by color, in the order in which each color appears first. func render(t Traits, o Options, s *syntax) string { g := buildGrid(t) bg := bgColors[t.Background] if o.BackgroundColor != "" { bg = hexColor(o.BackgroundColor) } var ( groupOf [128]uint8 // group of a key plus one, or 0 when the key was not seen groupColor [maxGroups]string groupPath [maxGroups]string groups int ) for y := 0; y < gridH; y++ { row := g[y*gridW : (y+1)*gridW] for x := 0; x < gridW; { c := row[x] if c == 0 { x++ continue } n := 1 for x+n < gridW && row[x+n] == c { n++ } gi := int(groupOf[c]) if gi == 0 { // First time that the key is seen, its color might be already used by another key color := colorOf(t, c) for i := 0; i < groups; i++ { if groupColor[i] == color { gi = i + 1 break } } if gi == 0 { groupColor[groups] = color groups++ gi = groups } groupOf[c] = uint8(gi) } gi-- groupPath[gi] += s.mHead[x] + nums[y] + runTail[n] x += n } } out := s.open if o.Size > 0 { px := strconv.Itoa(o.Size) out += s.widthOpen + px + s.heightOpen + px + s.sizeClose } out += s.openEnd if !o.NoBackground { out += s.rectOpen + bg + s.rectClose } for i := 0; i < groups; i++ { out += s.pathOpen + groupColor[i] + s.pathMid + groupPath[i] + s.pathClose } return out + s.end } // colorOf returns the color of a palette key, in hex digits. func colorOf(t Traits, key byte) string { if c := fixedColor(key); c != "" { return c } skin := int(t.Skin) * 3 hat := int(t.Hat) * 3 beard := int(t.BeardColor) * 3 tunic := int(t.Tunic) * 3 switch key { case 't': return skinColors[skin] case 's': return skinColors[skin+1] case 'u': return skinColors[skin+2] case 'h': return hatColors[hat] case 'H': return hatColors[hat+1] case 'j': return hatColors[hat+2] case 'w': return beardColors[beard] case 'W': return beardColors[beard+1] case 'x': return beardColors[beard+2] case 'c': return tunicColors[tunic] case 'C': return tunicColors[tunic+1] case 'd': return tunicColors[tunic+2] case 'g': return glassesColors[t.Glasses-1] } panic("unknown palette key " + string(rune(key))) } // hexColor checks a "#rrggbb" color and returns its hex digits. func hexColor(color string) string { if len(color) != 7 || color[0] != '#' { panic("invalid background color, expected #rrggbb") } for i := 1; i < 7; i++ { c := color[i] if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') { panic("invalid background color, expected #rrggbb") } } return color[1:] }
- #14traits.gno
- #15package nft import ( "crypto/sha256" "errors" ) var ( beardColorNames = [...]string{"White", "Grey", "Ginger", "Brown"} beardShapeNames = [...]string{"None", "Long", "Pointy", "Round"} backgroundNames = [...]string{"Sky", "Sand", "Mint", "Lilac", "Blush"} glassesNames = [...]string{"None", "Black", "Gold", "Red", "Teal"} hatNames = [...]string{"Red", "Blue", "Green", "Purple", "Orange"} skinNames = [...]string{"Light", "Medium", "Deep"} tunicNames = [...]string{"Sky", "Leaf", "Clay", "Rose", "Sun"} ) // Attribute is a named trait of a gnome. type Attribute struct { Type string Value string } // Traits describe a gnome. type Traits struct { Skin uint8 Hat uint8 BeardColor uint8 Tunic uint8 Background uint8 HatShape uint8 // 0 pointy, 1 bent Flip uint8 // 0 or 1, no effect on pointy hats Glasses uint8 Beard uint8 } // NewTraits derives the traits of the gnome from a seed. // A single seed always generate the same traits. // Use `Validate()` to check traits before drawing. func NewTraits(seed string) Traits { data := sha256.Sum256([]byte(seed)) t := Traits{ Skin: data[0] % 3, Hat: data[1] % 5, BeardColor: data[2] % 4, Tunic: data[3] % 5, Background: data[4] % 5, Flip: data[6] & 1, } // Gnome with bent hat, about 35% if data[5] < 90 { t.HatShape = 1 } // Gnome with glasses, about 30% if data[7] < 77 { t.Glasses = data[8]%4 + 1 } // Gnome without beard is a rare trait, about 4% if data[9] >= 10 { t.Beard = data[10]%3 + 1 } return t } // Validate checks that all the traits are in range. func (t Traits) Validate() error { switch { case t.Skin > 2: return errors.New("invalid skin") case t.Hat > 4: return errors.New("invalid hat color") case t.BeardColor > 3: return errors.New("invalid beard color") case t.Tunic > 4: return errors.New("invalid tunic") case t.Background > 4: return errors.New("invalid background") case t.HatShape > 1: return errors.New("invalid hat shape") case t.Flip > 1: return errors.New("invalid flip") case t.Glasses > 4: return errors.New("invalid glasses") case t.Beard > 3: return errors.New("invalid beard") } return nil } // Normalized returns the traits with the flip cleared when it has no effect. // Two traits that draw the same gnome have the same normalized traits, so // this is the value to compare and to keep when gnomes must be different. func (t Traits) Normalized() Traits { // Clear trait when hat is pointy. if t.HatShape == 0 { t.Flip = 0 } return t } // Attributes returns the traits of a gnome with their names. // It panics when traits are not valid. func (t Traits) Attributes() []Attribute { if err := t.Validate(); err != nil { panic(err.Error()) } hat := "Pointed" if t.HatShape == 1 { hat = "Bent tip (right)" if t.Flip == 1 { hat = "Bent tip (left)" } } return []Attribute{ {"Skin", skinNames[t.Skin]}, {"Hat color", hatNames[t.Hat]}, {"Hat", hat}, {"Beard color", beardColorNames[t.BeardColor]}, {"Beard", beardShapeNames[t.Beard]}, {"Tunic", tunicNames[t.Tunic]}, {"Glasses", glassesNames[t.Glasses]}, {"Background", backgroundNames[t.Background]}, } } // Pack packs the traits in 20 bits. func Pack(t Traits) uint32 { // Bits: // 0-1 skin // 2-4 hat // 5-6 beard color // 7-9 tunic // 10-12 background // 13 hat shape // 14 flip // 15-17 glasses // 18-19 beard return uint32(t.Skin) | uint32(t.Hat)<<2 | uint32(t.BeardColor)<<5 | uint32(t.Tunic)<<7 | uint32(t.Background)<<10 | uint32(t.HatShape)<<13 | uint32(t.Flip)<<14 | uint32(t.Glasses)<<15 | uint32(t.Beard)<<18 } // Unpack unpacks the traits. // It does not check the values, use FromPacked for untrusted input. func Unpack(v uint32) Traits { return Traits{ Skin: uint8(v & 3), Hat: uint8((v >> 2) & 7), BeardColor: uint8((v >> 5) & 3), Tunic: uint8((v >> 7) & 7), Background: uint8((v >> 10) & 7), HatShape: uint8((v >> 13) & 1), Flip: uint8((v >> 14) & 1), Glasses: uint8((v >> 15) & 7), Beard: uint8((v >> 18) & 3), } } // FromPacked unpacks traits and checks them. func FromPacked(v uint32) (Traits, error) { if v>>20 != 0 { return Traits{}, errors.New("invalid packed traits") } t := Unpack(v) if err := t.Validate(); err != nil { return Traits{}, err } return t, nil }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]