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

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}