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}