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 · 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, "&", "&")
334 s = strings.ReplaceAll(s, "<", "<")
335 s = strings.ReplaceAll(s, ">", ">")
336 return s
337}