skincard.gno
7.91 Kb · 226 lines
1package nsview
2
3import (
4 "strings"
5
6 "gno.land/p/nt/ufmt/v0"
7)
8
9/*
10THE SKIN'S OWN CARD — what a skin token looks like when it is the thing
11being listed, rather than the thing decorating a name.
12
13WHY IT EXISTS. A skin is a separate token (see SKINS.md §1), so it has
14its own listing on any marketplace that shows it. Drawn the way a name
15card is drawn — art edge to edge, big lettering over the top — the two
16would be indistinguishable, and somebody would buy a picture believing
17they had bought a name. That is not a subtle risk: it is the single most
18likely way this product hurts somebody.
19
20THE FRAME IS THE WHOLE IDEA. A name card is FULL BLEED; this one is
21MATTED, with the art inset inside a border and bands of ground above and
22below it. The distinction survives at thumbnail size, at a glance, with
23no text read at all — which is the only size and attention a marketplace
24grid actually gets. The word SKIN across the top is the confirmation, not
25the mechanism.
26
27IT NAMES ITSELF, and it says what it will not go on. An extension-locked
28skin that does not say so is a support ticket per sale.
29
30WHAT IT DELIBERATELY DOES NOT SHOW: a price, an edition number, a rarity,
31or anything else that frames the object as an investment. That rule is
32from the risk policy and it applies to the artwork as much as to the copy
33on the site — the card travels to places our words do not.
34*/
35
36// The matte. A name card uses all 400; this one is inset, and the inset
37// is the tell.
38const (
39 skinTop = 74 // the band the word SKIN sits in
40 skinFoot = 62 // the band the skin's own name sits in
41
42 // THE ART IS SQUARE, and its size is whatever the SHORTER dimension
43 // allows — which is the height, because the two label bands eat into
44 // it and the sides only give up a margin.
45 //
46 // This started as "340 wide" and was wrong: 340 is what the width
47 // allows, but only 264 of height remains once the bands are taken,
48 // so the art ran 76px down into the footer and sat behind the skin's
49 // own name. A square sprite drawn into a non-square box either
50 // stretches or overflows, and both are worse than being smaller.
51 skinBox = 400 - skinTop - skinFoot // 264
52 // Centred on what is left, so the side margins come out equal.
53 skinPad = (400 - skinBox) / 2 // 68
54)
55
56/*
57skinArtSVG renders a sprite to fill a box, INFERRING THE GRID from the
58data rather than assuming twelve.
59
60spriteSVG hardcodes twelve in four places, which is right for the domain
61artwork it was written for and wrong here: SKINS.md §2a offers 12, 16, 24
62and 32, and the whole point of inferring is that one format then covers
63every size and every sprite already on chain keeps working.
64
65Validation is spriteSVG's, unchanged and unrelaxed, because it is not
66merely a parser — it is the thing standing between community-authored
67data and markup inside somebody else's wallet. Any violation rejects the
68WHOLE sprite rather than skipping the bad part: a partial picture hides
69the fault.
70*/
71func skinArtSVG(enc string, ox, oy, box int) string {
72 if enc == "" {
73 return ""
74 }
75 parts := strings.Split(enc, "|")
76 n := len(parts) - 1
77 if n < 4 || n > 64 {
78 return ""
79 }
80 colours := map[string]string{}
81 for _, entry := range strings.Split(parts[0], ",") {
82 if len(entry) != 8 || entry[1] != '#' {
83 return ""
84 }
85 if !isHexColour(entry[1:]) {
86 return ""
87 }
88 colours[entry[:1]] = entry[1:]
89 }
90
91 /*
92 EVERY CELL IS PLACED FROM ITS OWN EXACT BOUNDARY, never from an
93 accumulated cell size.
94
95 The obvious way — cell := box / n, then x*cell — truncates once
96 and then repeats the error n times, so the art stops short of its
97 own frame by up to n pixels. Scaling the cell by a thousand first
98 does not fix it either: at n=12 in a 340 box the cell is 28.333,
99 28333 x 12 is 339996, and the last column still lands a pixel
100 inside the frame. Which is a full pixel of bare matte down one
101 edge, and by eye it reads as a design choice rather than a bug.
102
103 Computing each boundary as x*box/n instead makes the LAST one
104 exactly n*box/n = box, at every grid size, with no accumulation
105 to go wrong.
106
107 RUNS OF ONE COLOUR BECOME ONE RECT, which SKINS.md §2a asked for
108 before the grid went much past 32 and which the collection has now
109 made urgent rather than tidy.
110
111 Every painted cell used to emit its own <rect>, inside every
112 wallet that renders the card. That is tolerable for a sprite of a
113 thing and ruinous for a BACKGROUND: a vertical gradient is one
114 colour per row, so a 32-wide one emitted 1,024 rects to draw 32
115 distinct bands. Merging horizontal runs draws it in 32 — the same
116 picture, pixel for pixel, at a thirty-second of the markup.
117
118 It costs nothing when it does not apply. A busy sprite with no
119 horizontal runs merges nothing and emits exactly what it did
120 before.
121 */
122 var b strings.Builder
123 for y := 1; y <= n; y++ {
124 row := parts[y]
125 if len(row) != n {
126 return ""
127 }
128 y0 := oy + (y-1)*box/n
129 h := oy + y*box/n - y0
130 for x := 0; x < n; {
131 ch := row[x : x+1]
132 if ch == "." {
133 x++
134 continue
135 }
136 c, ok := colours[ch]
137 if !ok {
138 return ""
139 }
140 // How far this colour runs.
141 run := x + 1
142 for run < n && row[run:run+1] == ch {
143 run++
144 }
145 x0 := ox + x*box/n
146 w := ox + run*box/n - x0
147 b.WriteString(ufmt.Sprintf(
148 `<rect x="%d" y="%d" width="%d" height="%d" fill="%s"/>`,
149 x0, y0, w, h, c))
150 x = run
151 }
152 }
153 if b.Len() == 0 {
154 return ""
155 }
156 return b.String()
157}
158
159/*
160SkinCardSVG draws a skin token's own listing card.
161
162 name the skin's name, as the shop shows it
163 art the sprite string
164 lock the extension it is restricted to, or "" for one that goes
165 on any name
166 light true = pale lettering on a dark matte
167 false = dark lettering on a pale matte
168
169The return is a whole document rather than a fragment, because unlike the
170nameplate this composites over nothing — it IS the card.
171*/
172func SkinCardSVG(name, art, lock string, light bool) string {
173 ink, edge, matte := "#f4f4f5", "#26262b", "#0b0d10"
174 if !light {
175 ink, edge, matte = "#0b0d10", "#d4d4d8", "#f4f4f5"
176 }
177
178 var b strings.Builder
179 b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">`)
180 b.WriteString(ufmt.Sprintf(`<rect width="400" height="400" fill="%s"/>`, matte))
181
182 // THE WORD, big and flat across the top. Monospace and letter-spaced
183 // so it reads as a label on the object rather than as a title for it.
184 b.WriteString(ufmt.Sprintf(
185 `<text x="%d" y="46" font-family="monospace" font-size="27" font-weight="bold"`+
186 ` letter-spacing="7" fill="%s">SKIN</text>`, skinPad, ink))
187
188 // The art, inset, with a frame drawn AROUND it rather than over it —
189 // a border that overlapped would eat a row of somebody's pixels.
190 inner := skinArtSVG(art, skinPad, skinTop, skinBox)
191 if inner == "" {
192 // A skin whose art will not parse still has to render as
193 // something, or the listing is a blank square with no clue why.
194 b.WriteString(ufmt.Sprintf(
195 `<rect x="%d" y="%d" width="%d" height="%d" fill="none" stroke="%s"`+
196 ` stroke-width="2" stroke-dasharray="7 6"/>`,
197 skinPad, skinTop, skinBox, skinBox, edge))
198 b.WriteString(ufmt.Sprintf(
199 `<text x="200" y="%d" text-anchor="middle" font-family="monospace"`+
200 ` font-size="13" fill="%s">ART WILL NOT RENDER</text>`,
201 skinTop+skinBox/2, edge))
202 } else {
203 b.WriteString(inner)
204 }
205 b.WriteString(ufmt.Sprintf(
206 `<rect x="%d" y="%d" width="%d" height="%d" fill="none" stroke="%s" stroke-width="2"/>`,
207 skinPad-1, skinTop-1, skinBox+2, skinBox+2, edge))
208
209 // The skin's own name, and what it will not go on.
210 b.WriteString(ufmt.Sprintf(
211 `<text x="%d" y="%d" font-family="sans-serif" font-size="21" font-weight="bold"`+
212 ` fill="%s">%s</text>`,
213 skinPad, 400-skinFoot+34, ink, esc(name)))
214
215 tail := "goes on any name"
216 if lock != "" {
217 tail = "*" + lock + " names only"
218 }
219 b.WriteString(ufmt.Sprintf(
220 `<text x="%d" y="%d" text-anchor="end" font-family="monospace" font-size="12"`+
221 ` letter-spacing="1" fill="%s">%s</text>`,
222 400-skinPad, 400-skinFoot+34, edge, esc(strings.ToUpper(tail))))
223
224 b.WriteString(`</svg>`)
225 return b.String()
226}