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.

skins.gno

13.52 Kb · 337 lines
  1package nsview
  2
  3import (
  4	"strings"
  5	"time"
  6
  7	"gno.land/p/nt/ufmt/v0"
  8	"gno.land/r/nym-mnsrehearsal001/nsdata/v2"
  9)
 10
 11/*
 12SKINS — the card becomes something else entirely for a day.
 13
 14The decoration calendar already draws snow and embers ON TOP of a card.
 15A skin REPLACES it: on 22 May the token is a pizza, on 3 January it is a
 16newspaper. That is a different kind of change, so it is a different
 17field, and any date without one renders exactly as it always has.
 18
 19HOW A CONTRACT KNOWS THE DATE AT ALL. It does not read a block height and
 20guess. Every block carries a timestamp in its header, agreed by consensus
 21the same way its transactions are, and `time.Now()` inside a realm returns
 22that block time. So a realm has a real clock, accurate to within a block
 23interval — which for a decoration that changes once a year is precision
 24nobody will ever notice. This is the same mechanism the seasonal overlay
 25has always used.
 26
 27WHY THIS COSTS NOTHING TO RUN. The card is generated on every read rather
 28than fixed at mint, so a skin needs no transaction, no migration and
 29nobody awake at midnight. It applies to names sold years ago. Turning it
 30off is deleting one field from the calendar.
 31
 32NO FONTS. Every renderer that shows these — wallets, marketplaces — puts
 33the SVG in a sandbox with no network, so a web font simply fails and
 34silently substitutes. Only the generic families resolve, which is why the
 35existing card says monospace and why these say serif or sans-serif. The
 36display faces in the mockups are a later job: an alphabet drawn as paths
 37and stored once, which is the only way to get identical letterforms on
 38every device.
 39*/
 40
 41// skinFor returns the skin named by whichever calendar rule is active
 42// today, or "" for the ordinary card. Reusing pickRule means a skin
 43// obeys the same window rules, the same first-match-wins ordering and
 44// the same year wrapping as every other season.
 45func skinFor(now int64) string {
 46	return skinFromSpec(nsdata.GetGlobal(keyFX), now)
 47}
 48
 49// Split out so the calendar logic can be tested against a spec directly,
 50// without standing up a vault to hold one.
 51func skinFromSpec(spec string, now int64) string {
 52	rule := pickRule(spec, now)
 53	if rule == "" {
 54		return ""
 55	}
 56	return ruleField(rule, "skin")
 57}
 58
 59// hasSkin reports whether a name is renderable as this skin. Unknown
 60// names render nothing rather than a broken card — the same defensive
 61// stance the overlay parser takes, and for the same reason: this markup
 62// lands inside a token that other people's software renders.
 63func hasSkin(name string) bool {
 64	switch name {
 65	case "pizza", "paper", "cal", "fog", "slash", "pi", "heart":
 66		return true
 67	}
 68	return false
 69}
 70
 71// seedOf turns a token id into a small number, so that a card can differ
 72// from the one beside it without any randomness — which a deterministic
 73// machine does not have. Every node computes the same answer for the same
 74// name, which is the requirement; looking arbitrary is merely the effect.
 75func seedOf(tid string) int {
 76	h := 2166136261
 77	for i := 0; i < len(tid); i++ {
 78		h ^= int(tid[i])
 79		h *= 16777619
 80		h &= 0x7fffffff
 81	}
 82	return h
 83}
 84
 85// skinSVG renders a complete 400x400 card. Same frame as the ordinary
 86// one, so nothing downstream has to know which it received.
 87func skinSVG(name, tid string, now int64) string {
 88	label := tid
 89	if at := strings.Index(tid, "*"); at > 0 {
 90		label = tid
 91	}
 92	size := 34
 93	if len(label) > 14 {
 94		size = 22
 95	} else if len(label) > 10 {
 96		size = 27
 97	}
 98	switch name {
 99	case "pizza":
100		return skinPizza(label, size, seedOf(tid))
101	case "paper":
102		return skinPaper(label, size, now)
103	case "cal":
104		return skinCal(label, size)
105	case "fog":
106		return skinFog(label, size)
107	case "slash":
108		return skinSlash(label, size)
109	case "pi":
110		return skinPi(label, size)
111	case "heart":
112		return skinHeart(label, size)
113	}
114	return ""
115}
116
117func head(bg string) string {
118	return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img">` +
119		`<rect width="400" height="400" fill="` + bg + `"/>`
120}
121
122func foot() string {
123	return `<text x="200" y="380" text-anchor="middle" font-family="monospace" font-size="10"` +
124		` fill="#ffffff" opacity="0.45">MINTED ON GNO.LAND</text></svg>`
125}
126
127/* ---------------- 22 May: the pizza ----------------
128 *
129 * Eight wedges with a gap between them, so it reads as CUT rather than
130 * merely divided, and each wedge carries its own toppings inside its own
131 * group — that is what makes a slice leave with its pepperoni still on
132 * it instead of leaving them floating over the board.
133 *
134 * EVERY NAME EATS IN ITS OWN ORDER. The seed decides which slice goes
135 * first and how long each one waits, so a marketplace full of these is a
136 * room full of pizzas being eaten from different sides at different
137 * rates rather than one animation repeated. It is derived from the token
138 * id, so it is the same on every node and different for every name.
139 */
140func skinPizza(label string, size, seed int) string {
141	var b strings.Builder
142	b.WriteString(head("#2a2018"))
143	b.WriteString(`<circle cx="200" cy="200" r="168" fill="#3a2c20"/>`)
144
145	order := make([]int, 8)
146	for i := range order {
147		order[i] = i
148	}
149	// A deterministic shuffle: the seed walks the list and swaps.
150	s := seed
151	for i := 7; i > 0; i-- {
152		s = (s*1103515245 + 12345) & 0x7fffffff
153		j := s % (i + 1)
154		order[i], order[j] = order[j], order[i]
155	}
156
157	for pos, idx := range order {
158		a0 := float64(idx)*45 + 1.8
159		a1 := float64(idx+1)*45 - 1.8
160		// Each slice waits a little longer than the last, with the gap
161		// itself varying by name so two cards never keep step.
162		begin := 3 + pos*3 + (seed>>uint(pos))%4
163		b.WriteString(`<g>`)
164		b.WriteString(`<animate attributeName="opacity" values="1;1;0" keyTimes="0;0.5;1"`)
165		b.WriteString(ufmt.Sprintf(` dur="%ds" begin="%ds" fill="freeze"/>`, 2, begin))
166		b.WriteString(wedge(200, 200, 162, a0, a1, "#d9822b"))
167		b.WriteString(wedge(200, 200, 143, a0+1.2, a1-1.2, "#f0b23c"))
168		// Three toppings inside this wedge: one pepperoni, two herbs.
169		for t := 0; t < 3; t++ {
170			s = (s*1103515245 + 12345) & 0x7fffffff
171			ang := a0 + 4 + float64(s%int(a1-a0-8))
172			s = (s*1103515245 + 12345) & 0x7fffffff
173			rad := 46 + float64(s%86)
174			x, y := polar(200, 200, rad, ang)
175			if t == 0 {
176				b.WriteString(ufmt.Sprintf(`<circle cx="%d" cy="%d" r="13" fill="#b8342a"/>`, x, y))
177			} else {
178				b.WriteString(ufmt.Sprintf(
179					`<rect x="%d" y="%d" width="13" height="13" rx="3" fill="#4e8b3a"/>`, x-6, y-6))
180			}
181		}
182		b.WriteString(`</g>`)
183	}
184	b.WriteString(ufmt.Sprintf(
185		`<text x="200" y="212" text-anchor="middle" font-family="sans-serif" font-size="%d"`+
186			` font-weight="bold" fill="#2a1405">%s</text>`, size, esc(label)))
187	b.WriteString(foot())
188	return b.String()
189}
190
191func wedge(cx, cy int, r float64, a0, a1 float64, fill string) string {
192	x0, y0 := polar(cx, cy, r, a0)
193	x1, y1 := polar(cx, cy, r, a1)
194	return ufmt.Sprintf(`<path d="M%d %d L%d %d A%d %d 0 0 1 %d %d Z" fill="%s"/>`,
195		cx, cy, x0, y0, int(r), int(r), x1, y1, fill)
196}
197
198// Degrees clockwise from twelve o'clock, which is how a pizza is cut.
199// Integer maths throughout: a realm has no floating point worth trusting
200// across nodes, and a pizza does not need sub-pixel accuracy.
201func polar(cx, cy int, r float64, deg float64) (int, int) {
202	rad := (deg - 90) * 3.14159265 / 180
203	return cx + int(r*cosApprox(rad)), cy + int(r*sinApprox(rad))
204}
205
206func sinApprox(x float64) float64 {
207	for x > 3.14159265 {
208		x -= 6.2831853
209	}
210	for x < -3.14159265 {
211		x += 6.2831853
212	}
213	x2 := x * x
214	return x * (1 - x2/6 + x2*x2/120 - x2*x2*x2/5040)
215}
216
217func cosApprox(x float64) float64 { return sinApprox(x + 1.5707963) }
218
219/* ---------------- 3 Jan: the front page ---------------- */
220func skinPaper(label string, size int, now int64) string {
221	year := time.Unix(now, 0).UTC().Year()
222	return head("#f4f0e4") +
223		`<text x="200" y="66" text-anchor="middle" font-family="serif" font-size="30"` +
224		` font-weight="bold" fill="#141210">THE LEDGER</text>` +
225		`<line x1="40" y1="80" x2="360" y2="80" stroke="#141210" stroke-width="2"/>` +
226		ufmt.Sprintf(`<text x="200" y="100" text-anchor="middle" font-family="monospace"`+
227			` font-size="11" fill="#5a5348">3 JANUARY %d &#183; BLOCK ZERO</text>`, year) +
228		ufmt.Sprintf(`<text x="200" y="176" text-anchor="middle" font-family="serif"`+
229			` font-size="%d" font-weight="bold" fill="#141210">%s</text>`, size+6, esc(label)) +
230		`<text x="200" y="214" text-anchor="middle" font-family="sans-serif" font-size="13"` +
231		` fill="#39342c">The first block is mined. A chain begins.</text>` +
232		`<text x="200" y="234" text-anchor="middle" font-family="sans-serif" font-size="13"` +
233		` fill="#39342c">Years later somebody registers a name</text>` +
234		`<text x="200" y="254" text-anchor="middle" font-family="sans-serif" font-size="13"` +
235		` fill="#39342c">on it that does nothing at all.</text>` +
236		`<line x1="40" y1="278" x2="360" y2="278" stroke="#b9b2a2" stroke-width="1"/>` +
237		`<text x="200" y="380" text-anchor="middle" font-family="monospace" font-size="10"` +
238		` fill="#5a5348">MINTED ON GNO.LAND</text></svg>`
239}
240
241/* ---------------- 29 Feb: the calendar page ---------------- */
242func skinCal(label string, size int) string {
243	return head("#ffffff") +
244		`<rect width="400" height="72" fill="#c0392b"/>` +
245		`<text x="200" y="49" text-anchor="middle" font-family="sans-serif" font-size="26"` +
246		` font-weight="bold" fill="#ffffff" letter-spacing="6">FEBRUARY</text>` +
247		`<circle cx="150" cy="24" r="9" fill="#f7f4ee" stroke="#00000055" stroke-width="3"/>` +
248		`<circle cx="250" cy="24" r="9" fill="#f7f4ee" stroke="#00000055" stroke-width="3"/>` +
249		`<text x="200" y="252" text-anchor="middle" font-family="sans-serif" font-size="170"` +
250		` font-weight="bold" fill="#e8e3d9">29</text>` +
251		ufmt.Sprintf(`<text x="200" y="300" text-anchor="middle" font-family="monospace"`+
252			` font-size="%d" font-weight="bold" fill="#1a1a1a">%s</text>`, size, esc(label)) +
253		`<text x="200" y="380" text-anchor="middle" font-family="monospace" font-size="10"` +
254		` fill="#8a857a">MINTED ON GNO.LAND</text></svg>`
255}
256
257/* ---------------- 20 Apr: the fog ---------------- */
258func skinFog(label string, size int) string {
259	return head("#1d4028") +
260		`<defs><radialGradient id="g"><stop offset="0" stop-color="#8fd48a"/>` +
261		`<stop offset="0.68" stop-color="#3f7a4a"/><stop offset="1" stop-color="#1d4028"/>` +
262		`</radialGradient><filter id="b"><feGaussianBlur stdDeviation="26"/></filter></defs>` +
263		`<rect width="400" height="400" fill="url(#g)"/>` +
264		`<g filter="url(#b)" opacity="0.5">` +
265		`<circle cx="130" cy="140" r="90" fill="#ffffff" opacity="0.5">` +
266		`<animate attributeName="cx" values="130;190;130" dur="18s" repeatCount="indefinite"/>` +
267		`</circle>` +
268		`<circle cx="280" cy="250" r="80" fill="#ffffff" opacity="0.35">` +
269		`<animate attributeName="cy" values="250;200;250" dur="22s" repeatCount="indefinite"/>` +
270		`</circle></g>` +
271		ufmt.Sprintf(`<text x="200" y="212" text-anchor="middle" font-family="sans-serif"`+
272			` font-size="%d" font-weight="bold" fill="#f2fff2">%s</text>`, size, esc(label)) +
273		foot()
274}
275
276/* ---------------- Friday 13th: the poster ---------------- */
277func skinSlash(label string, size int) string {
278	return head("#15080a") +
279		`<path d="M88 -20 L112 420 L96 420 L72 -20 Z" fill="#c42c24" opacity="0.85"/>` +
280		`<path d="M252 420 L268 -20 L278 -20 L262 420 Z" fill="#8e1d18" opacity="0.7"/>` +
281		ufmt.Sprintf(`<text x="200" y="212" text-anchor="middle" font-family="serif"`+
282			` font-size="%d" font-weight="bold" fill="#d8342c">%s</text>`, size+4, esc(label)) +
283		foot()
284}
285
286/* ---------------- 14 Mar: pi ---------------- */
287func skinPi(label string, size int) string {
288	digits := []string{
289		"3.14159265358979323846", "26433832795028841971", "69399375105820974944",
290		"59230781640628620899", "86280348253421170679", "82148086513282306647",
291	}
292	var b strings.Builder
293	b.WriteString(head("#101418"))
294	for i, row := range digits {
295		b.WriteString(ufmt.Sprintf(
296			`<text x="26" y="%d" font-family="monospace" font-size="30" fill="#2b4756">%s</text>`,
297			70+i*56, row))
298	}
299	b.WriteString(`<rect x="52" y="176" width="296" height="52" rx="8" fill="#101418"/>`)
300	b.WriteString(ufmt.Sprintf(
301		`<text x="200" y="212" text-anchor="middle" font-family="monospace" font-size="%d"`+
302			` font-weight="bold" fill="#eaf2f6">%s</text>`, size, esc(label)))
303	b.WriteString(foot())
304	return b.String()
305}
306
307/* ---------------- 14 Feb: the heart ----------------
308 *
309 * A real double thump, not one pulse: squeeze, small recoil, second
310 * smaller squeeze, then a long rest. The rest is most of what makes it
311 * read as a heartbeat rather than a throb.
312 */
313func skinHeart(label string, size int) string {
314	return head("#fbe3ec") +
315		`<g transform="translate(200,196)">` +
316		`<animateTransform attributeName="transform" type="scale"` +
317		` values="1;1.085;0.995;1.05;1;1" keyTimes="0;0.09;0.18;0.27;0.40;1"` +
318		` dur="1.15s" repeatCount="indefinite" additive="sum"/>` +
319		`<path transform="translate(-150,-140)" fill="#f7c3d5" stroke="#e39bb5" stroke-width="5"` +
320		` d="M150 258 C66 198 24 150 24 102 A60 60 0 0 1 150 72 A60 60 0 0 1 276 102` +
321		` C276 150 234 198 150 258 Z"/></g>` +
322		ufmt.Sprintf(`<text x="200" y="212" text-anchor="middle" font-family="sans-serif"`+
323			` font-size="%d" font-weight="bold" fill="#7e1f3d" letter-spacing="2">%s</text>`,
324			size, esc(strings.ToUpper(label))) +
325		`<text x="200" y="380" text-anchor="middle" font-family="monospace" font-size="10"` +
326		` fill="#b03a62" opacity="0.7">MINTED ON GNO.LAND</text></svg>`
327}
328
329// Markup safety, same as everywhere else that puts a name into SVG: a
330// label is validated on registration, but this is the last gate before
331// it lands inside somebody else's renderer.
332func esc(s string) string {
333	s = strings.ReplaceAll(s, "&", "&amp;")
334	s = strings.ReplaceAll(s, "<", "&lt;")
335	s = strings.ReplaceAll(s, ">", "&gt;")
336	return s
337}