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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

svg.gno

6.02 Kb · 231 lines
  1package nft
  2
  3import (
  4	"encoding/base64"
  5	"strconv"
  6)
  7
  8// CanonicalSize is the width and height in pixels of the image used for NFTs.
  9const CanonicalSize = 512
 10
 11// Options are the options to render a gnome.
 12type Options struct {
 13	// Size is the width and the height of the image in pixels, because the image is a square.
 14	// Multiples of 32 keep every art pixel as a whole number of screen pixels.
 15	// Zero omits the width and height, so that the image scales to its container.
 16	Size int
 17
 18	// NoBackground omits the background.
 19	NoBackground bool
 20
 21	// BackgroundColor replaces the color of the background, in "#rrggbb" format.
 22	BackgroundColor string
 23}
 24
 25// RenderSVG renders the gnome as an SVG document.
 26// Traits must be valid.
 27// The same traits and options always give the same bytes.
 28func RenderSVG(t Traits, o Options) string {
 29	return render(t, o, &rawSyntax)
 30}
 31
 32// RenderSeed renders the gnome of a seed as an SVG document.
 33func RenderSeed(seed string, o Options) string {
 34	return RenderSVG(NewTraits(seed), o)
 35}
 36
 37// RenderDataURI renders the gnome as a "data:image/svg+xml;charset=utf-8," URI, percent-encoded.
 38//
 39// Cheapest way to embed a gnome.
 40// SVG is written encoded, instead of encoding it afterwards.
 41// Traits must be valid.
 42func RenderDataURI(t Traits, o Options) string {
 43	return "data:image/svg+xml;charset=utf-8," + render(t, o, &uriSyntax)
 44}
 45
 46// DataURI encodes an SVG document as a "data:image/svg+xml;base64," URI.
 47// It is slower than RenderDataURI, use it when Base64 is needed.
 48func DataURI(svg string) string {
 49	return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))
 50}
 51
 52// syntax define the fixed pieces of the SVG document, that are the same for every gnome.
 53type syntax struct {
 54	open       string // start of the svg element, up to its size
 55	widthOpen  string
 56	heightOpen string
 57	sizeClose  string
 58	openEnd    string // end of the start of the svg element
 59	rectOpen   string // background, before its color
 60	rectClose  string
 61	pathOpen   string // before the color
 62	pathMid    string // after the color, before the segments
 63	pathClose  string
 64	end        string
 65	mHead      []string // start of a segment, by x
 66}
 67
 68// rawSyntax contains the fixed pieces of the SVG document.
 69var rawSyntax = syntax{
 70	open:       `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 0 32 32"`,
 71	widthOpen:  ` width="`,
 72	heightOpen: `" height="`,
 73	sizeClose:  `"`,
 74	openEnd:    ` shape-rendering="crispEdges">`,
 75	rectOpen:   `<rect x="-4" width="32" height="32" fill="#`,
 76	rectClose:  `"/>`,
 77	pathOpen:   `<path fill="#`,
 78	pathMid:    `" d="`,
 79	pathClose:  `"/>`,
 80	end:        `</svg>`,
 81	mHead:      rawMHead[:],
 82}
 83
 84// uriSyntax contains the fixes pieces of the SVG document encoded for data URI.
 85var uriSyntax = syntax{
 86	open:       `%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%22-4%200%2032%2032%22`,
 87	widthOpen:  `%20width=%22`,
 88	heightOpen: `%22%20height=%22`,
 89	sizeClose:  `%22`,
 90	openEnd:    `%20shape-rendering=%22crispEdges%22%3E`,
 91	rectOpen:   `%3Crect%20x=%22-4%22%20width=%2232%22%20height=%2232%22%20fill=%22%23`,
 92	rectClose:  `%22/%3E`,
 93	pathOpen:   `%3Cpath%20fill=%22%23`,
 94	pathMid:    `%22%20d=%22`,
 95	pathClose:  `%22/%3E`,
 96	end:        `%3C/svg%3E`,
 97	mHead:      uriMHead[:],
 98}
 99
100// maxGroups is the biggest number of different colors of a gnome: 22 keys.
101const maxGroups = 24
102
103// render draws the gnome:
104//   - Pixels are scanned row by row, and horizontal runs of the same key are one segment.
105//   - Segments are grouped by color, in the order in which each color appears first.
106func render(t Traits, o Options, s *syntax) string {
107	g := buildGrid(t)
108	bg := bgColors[t.Background]
109	if o.BackgroundColor != "" {
110		bg = hexColor(o.BackgroundColor)
111	}
112
113	var (
114		groupOf    [128]uint8 // group of a key plus one, or 0 when the key was not seen
115		groupColor [maxGroups]string
116		groupPath  [maxGroups]string
117		groups     int
118	)
119	for y := 0; y < gridH; y++ {
120		row := g[y*gridW : (y+1)*gridW]
121		for x := 0; x < gridW; {
122			c := row[x]
123			if c == 0 {
124				x++
125				continue
126			}
127
128			n := 1
129			for x+n < gridW && row[x+n] == c {
130				n++
131			}
132
133			gi := int(groupOf[c])
134			if gi == 0 {
135				// First time that the key is seen, its color might be already used by another key
136				color := colorOf(t, c)
137				for i := 0; i < groups; i++ {
138					if groupColor[i] == color {
139						gi = i + 1
140						break
141					}
142				}
143
144				if gi == 0 {
145					groupColor[groups] = color
146					groups++
147					gi = groups
148				}
149
150				groupOf[c] = uint8(gi)
151			}
152
153			gi--
154			groupPath[gi] += s.mHead[x] + nums[y] + runTail[n]
155			x += n
156		}
157	}
158
159	out := s.open
160	if o.Size > 0 {
161		px := strconv.Itoa(o.Size)
162		out += s.widthOpen + px + s.heightOpen + px + s.sizeClose
163	}
164
165	out += s.openEnd
166	if !o.NoBackground {
167		out += s.rectOpen + bg + s.rectClose
168	}
169
170	for i := 0; i < groups; i++ {
171		out += s.pathOpen + groupColor[i] + s.pathMid + groupPath[i] + s.pathClose
172	}
173	return out + s.end
174}
175
176// colorOf returns the color of a palette key, in hex digits.
177func colorOf(t Traits, key byte) string {
178	if c := fixedColor(key); c != "" {
179		return c
180	}
181
182	skin := int(t.Skin) * 3
183	hat := int(t.Hat) * 3
184	beard := int(t.BeardColor) * 3
185	tunic := int(t.Tunic) * 3
186
187	switch key {
188	case 't':
189		return skinColors[skin]
190	case 's':
191		return skinColors[skin+1]
192	case 'u':
193		return skinColors[skin+2]
194	case 'h':
195		return hatColors[hat]
196	case 'H':
197		return hatColors[hat+1]
198	case 'j':
199		return hatColors[hat+2]
200	case 'w':
201		return beardColors[beard]
202	case 'W':
203		return beardColors[beard+1]
204	case 'x':
205		return beardColors[beard+2]
206	case 'c':
207		return tunicColors[tunic]
208	case 'C':
209		return tunicColors[tunic+1]
210	case 'd':
211		return tunicColors[tunic+2]
212	case 'g':
213		return glassesColors[t.Glasses-1]
214	}
215	panic("unknown palette key " + string(rune(key)))
216}
217
218// hexColor checks a "#rrggbb" color and returns its hex digits.
219func hexColor(color string) string {
220	if len(color) != 7 || color[0] != '#' {
221		panic("invalid background color, expected #rrggbb")
222	}
223
224	for i := 1; i < 7; i++ {
225		c := color[i]
226		if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') {
227			panic("invalid background color, expected #rrggbb")
228		}
229	}
230	return color[1:]
231}