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.

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}