Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}