art.gno
6.25 Kb · 196 lines
1package home
2
3import (
4 "strconv"
5
6 "gno.land/p/nym-alexiscolin000/gnoradio/svg/v0"
7 "gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
8)
9
10// Sober Swiss artwork, drawn on-chain as SVG and embedded by gnoweb as
11// data-URI images: black and off-white, warm greys and the app's red, blue
12// and yellow accents. Cards with text sit on ink so they read in light and
13// dark mode alike.
14// Written with a strings.Builder to keep vm/qrender gas low.
15
16const (
17 ink = "#0d0d0d"
18 paper = "#fafaf8"
19 cream = "#f2efe9"
20 grey = "#c4c4c0"
21 stone = "#8f8b84"
22 line = "#e3e3df"
23 red = "#e5432f"
24 blue = "#2445c9"
25 amber = "#f2b51d" // the app's yellow
26 dim = "#3a3a37" // unplayed ticks on ink
27 font = "font-family:Geist,'Helvetica Neue',Helvetica,Arial,sans-serif"
28)
29
30func newCanvas(w, h int) *svg.Canvas { return svg.New(w, h, "text{"+font+"}") }
31
32// text writes s (escaped); weight 400–800; anchor start|middle|end.
33func drawText(c *svg.Canvas, x, y int, s, fill string, size, weight int, anchor, extra string) {
34 c.Text(x, y, s, fill, size, ` font-weight="`+text.Itoa(weight)+`" text-anchor="`+anchor+`"`+extra)
35}
36
37// coverSVG draws one of six geometric compositions and one of three accents,
38// picked from the track and its artist so neighbours differ.
39func coverSVG(id, artist int, title string, size int) string {
40 h := svg.FNV(strconv.Itoa(id) + "/" + strconv.Itoa(artist) + title)
41 accent := [3]string{red, blue, amber}[h/6%3]
42 c := newCanvas(size, size)
43 s := size
44 switch h % 6 {
45 case 0:
46 c.Rect(0, 0, s, s, s/12, ink, "")
47 c.Circle(s/2, s/2, s*28/100, accent)
48 case 1:
49 c.Rect(0, 0, s, s, s/12, grey, "")
50 c.Circle(s/5, s*9/10, s*45/100, stone)
51 c.Circle(s*68/100, s*32/100, s*15/100, ink)
52 case 2:
53 c.Rect(0, 0, s, s, s/12, accent, "")
54 c.Raw(`<path d="M0 ` + text.Itoa(s/2) + `H` + text.Itoa(s) + `V` + text.Itoa(s-s/12) + `Q` + text.Itoa(s) + ` ` + text.Itoa(s) + ` ` + text.Itoa(s-s/12) + ` ` + text.Itoa(s) +
55 `H` + text.Itoa(s/12) + `Q0 ` + text.Itoa(s) + ` 0 ` + text.Itoa(s-s/12) + `Z" fill="` + cream + `"/>`)
56 c.Circle(s/2, s/2, s*15/100, ink)
57 case 3:
58 c.Rect(0, 0, s, s, s/12, cream, "")
59 c.Rect(s*14/100, s*14/100, s*44/100, s*72/100, s*22/100, stone, "")
60 c.Circle(s*74/100, s*27/100, s*13/100, accent)
61 case 4:
62 c.Rect(0, 0, s, s, s/12, stone, "")
63 c.Raw(`<path d="M` + text.Itoa(s/12) + ` 0H` + text.Itoa(s/2) + `V` + text.Itoa(s) + `H` + text.Itoa(s/12) + `Q0 ` + text.Itoa(s) + ` 0 ` + text.Itoa(s-s/12) +
64 `V` + text.Itoa(s/12) + `Q0 0 ` + text.Itoa(s/12) + ` 0Z" fill="` + ink + `"/>`)
65 c.Circle(s/2, s/2, s/5, cream)
66 default:
67 c.Rect(0, 0, s, s, s/12, ink, "")
68 for i := 0; i < 21; i++ {
69 th := s / 14
70 if i%5 == 0 {
71 th = s / 7
72 }
73 c.Rect(s/10+i*(s*8/10)/20, s*62/100-th, 2, th, 0, paper, "")
74 }
75 c.Rect(s/10+int(h%17)*(s*8/10)/20, s*36/100, 3, s*30/100, 0, accent, "")
76 }
77 return c.Image("cover of " + title)
78}
79
80// onAirSVG is the station banner: huge time numerals and a tick ruler with
81// the red needle, as in the app, on ink so it reads in dark mode too.
82func onAirSVG(station, title, artist string, offset, dur int64) string {
83 const w, h = 720, 330
84 c := newCanvas(w, h)
85 c.Rect(0, 0, w, h, 26, ink, "")
86 c.Rect(28, 26, 104, 30, 10, red, "")
87 drawText(c, 80, 46, "ON AIR", "#fff", 13, 600, "middle", ` letter-spacing="1.5"`)
88 c.Rect(142, 26, 18+len(station)*9, 30, 10, dim, "")
89 drawText(c, 151, 46, station, paper, 13, 500, "start", "")
90 if title == "" {
91 drawText(c, w/2, 170, "Silence. Publish the first track.", grey, 26, 600, "middle", "")
92 return c.Image("nothing on air")
93 }
94 t := clock(offset)
95 if offset < 600 {
96 t = "0" + t
97 }
98 lead := 0
99 for lead < len(t) && (t[lead] == '0' || t[lead] == ':') {
100 lead++
101 }
102 c.Raw(`<text x="` + text.Itoa(w/2) + `" y="150" font-size="96" font-weight="500" text-anchor="middle" letter-spacing="-4">` +
103 `<tspan fill="` + dim + `">` + t[:lead] + `</tspan><tspan fill="` + paper + `">` + t[lead:] + `</tspan></text>`)
104 drawText(c, w/2, 178, "of "+clock(dur)+" · synced to the last block", stone, 13, 400, "middle", "")
105 frac := 0
106 if dur > 0 {
107 frac = int(offset * 1000 / dur)
108 }
109 for i := 0; i <= 80; i++ {
110 th, col := 10, dim
111 if i%10 == 0 {
112 th = 26
113 } else if i%5 == 0 {
114 th = 17
115 }
116 if i*1000/80 <= frac {
117 col = paper
118 }
119 c.Rect(40+i*8, 238-th, 2, th, 0, col, "")
120 }
121 c.Rect(40+frac*640/1000, 196, 2, 50, 0, red, "")
122 // Large enough to read once the card shrinks to a phone's width.
123 drawText(c, 40, 284, svg.Clip(title, 36), paper, 28, 600, "start", "")
124 drawText(c, 40, 312, svg.Clip(artist, 48), grey, 18, 400, "start", "")
125 return c.Image("on air: " + title)
126}
127
128// avatarSVG is a symmetric identicon in the sober palette.
129func avatarSVG(seed string, size int) string {
130 h := svg.FNV(seed)
131 c := newCanvas(size, size)
132 c.Rect(0, 0, size, size, size/2, cream, "")
133 cell := size / 9
134 for y := 0; y < 5; y++ {
135 for x := 0; x < 3; x++ {
136 h ^= uint32(y*3 + x + 1)
137 h *= 16777619
138 if (h>>24)%2 == 0 {
139 continue
140 }
141 col := ink
142 if (h>>20)%7 == 0 {
143 col = red
144 }
145 c.Rect(cell*2+x*cell, cell*2+y*cell, cell, cell, 2, col, "")
146 if x < 2 {
147 c.Rect(cell*2+(4-x)*cell, cell*2+y*cell, cell, cell, 2, col, "")
148 }
149 }
150 }
151 return c.Image("avatar")
152}
153
154// badgeSVG is the early-listener badge.
155func badgeSVG(title string, rank int) string {
156 c := newCanvas(120, 120)
157 c.Circle(60, 60, 58, ink)
158 c.Raw(`<circle cx="60" cy="60" r="50" fill="none" stroke="` + paper + `" stroke-width="1" stroke-dasharray="3 4"/>`)
159 drawText(c, 60, 50, "EARLY", grey, 11, 600, "middle", ` letter-spacing="2"`)
160 drawText(c, 60, 82, "#"+text.Itoa(rank), paper, 30, 500, "middle", "")
161 c.Circle(60, 100, 4, red)
162 return c.Image("early listener #" + text.Itoa(rank) + " of " + title)
163}
164
165type bar struct {
166 label string
167 value int64
168}
169
170// barsSVG draws a horizontal bar chart.
171func barsSVG(rows []bar, unit func(int64) string) string {
172 h := 30 + len(rows)*40
173 c := newCanvas(640, h)
174 c.Rect(0, 0, 640, h, 20, ink, "")
175 var max int64 = 1
176 for _, r := range rows {
177 if r.value > max {
178 max = r.value
179 }
180 }
181 for i, r := range rows {
182 y := 20 + i*40
183 w := int(r.value * 300 / max)
184 if w < 4 {
185 w = 4
186 }
187 col := paper
188 if i == 0 {
189 col = red
190 }
191 drawText(c, 20, y+20, svg.Clip(r.label, 20), paper, 14, 500, "start", "")
192 c.Rect(210, y+6, w, 18, 9, col, "")
193 drawText(c, 220+w, y+20, unit(r.value), grey, 13, 400, "start", "")
194 }
195 return c.Image("chart")
196}