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.58 Kb · 198 lines
  1package nft
  2
  3import (
  4	"encoding/base64"
  5	"strconv"
  6	"time"
  7
  8	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
  9	"gno.land/p/nym-alexiscolin000/gnoradio/svg/v0"
 10	"gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
 11)
 12
 13// Tickets are drawn on-chain as SVG in the GnoRadio sober style: white,
 14// black and greys with a single red accent. The perforation is cut out with a
 15// mask so it reads as holes on any page background. Text is escaped by
 16// svg.Text. Nothing extra is stored: a ticket is drawn from its records.
 17// The artist's name and visibility are read from the catalog's artist
 18// records in the data realm (nft.gno, CArtists).
 19
 20const (
 21	ink    = "#0d0d0d"
 22	paper  = "#fafaf8"
 23	grey   = "#9a9a96"
 24	accent = "#e5432f"
 25)
 26
 27// removedTitle replaces the details of a concert hidden by moderation.
 28const removedTitle = "Removed by moderation"
 29
 30// concert is what a ticket shows of its concert.
 31type concert struct {
 32	id, artist, capacity int
 33	title, venue         string
 34	start, price         int64
 35	cancelled            bool
 36}
 37
 38// shown decodes an event record for a ticket: everything while the concert
 39// and its artist are visible, a neutral placeholder otherwise. ok is false
 40// when the concert is not visible.
 41func shown(id int, rec string) (c concert, ok bool) {
 42	if rec == "" {
 43		return concert{title: removedTitle}, false
 44	}
 45	// store.Field reads "" past a record's end, and texts are checked again
 46	// here: whatever a later release writes, the metadata is valid JSON (a
 47	// record that is not a store.Rec may also make the read panic).
 48	c = concert{id: id, start: text.Atoi64(store.Field(rec, EStart)), capacity: text.Atoi(store.Field(rec, ECapacity)),
 49		cancelled: store.Field(rec, ECancelled) != ""}
 50	artist, title, venue := text.Atoi(store.Field(rec, EArtist)), store.Field(rec, ETitle), store.Field(rec, EVenue)
 51	if store.Field(rec, EHidden) != "" || !artistVisible(artist) || !text.Valid(title, 1, 64) || !text.Valid(venue, 0, 64) {
 52		c.title = removedTitle
 53		return c, false
 54	}
 55	c.artist, c.title, c.venue, c.price = artist, title, venue, text.Atoi64(store.Field(rec, EPrice))
 56	return c, true
 57}
 58
 59// fields reads ticket id's event, serial and attended mark (ok false when
 60// unknown).
 61func fields(id int) (ev, serial int, attended, ok bool) {
 62	rec := ticket(id)
 63	if rec == "" {
 64		return 0, 0, false, false
 65	}
 66	return text.Atoi(store.Field(rec, TEvent)), text.Atoi(store.Field(rec, TSerial)), store.Field(rec, TAttended) != "", true
 67}
 68
 69// EventSVG is the specimen ticket of a visible concert as a markdown image,
 70// or "" when the concert is not visible.
 71func EventSVG(id int) string {
 72	c, ok := shown(id, event(id))
 73	if !ok {
 74		return ""
 75	}
 76	return draw(c, 0, false).Image("ticket for " + c.title)
 77}
 78
 79// TicketSVG is a ticket as a markdown image, or "" if unknown.
 80func TicketSVG(id int) string {
 81	ev, serial, attended, ok := fields(id)
 82	if !ok {
 83		return ""
 84	}
 85	c, _ := shown(ev, event(ev))
 86	return draw(c, serial, attended).Image("ticket for " + c.title)
 87}
 88
 89// TokenURI is a ticket's GRC721 metadata as a data URI, its artwork drawn on
 90// the fly. It panics for an unknown ticket.
 91func TokenURI(tokenID string) string {
 92	id := text.Atoi(tokenID)
 93	ev, serial, attended, ok := fields(id)
 94	if !ok || strconv.Itoa(id) != tokenID {
 95		panic("nft: unknown ticket")
 96	}
 97	c, _ := shown(ev, event(ev))
 98	meta := `{"name":` + text.Str("GnoRadio · "+c.title+" #"+pad3(serial)) +
 99		`,"description":"GnoRadio concert ticket, drawn on-chain on gno.land","image":` + text.Str(draw(c, serial, attended).Data()) +
100		`,"attributes":[{"trait_type":"event","value":` + strconv.Itoa(c.id) +
101		`},{"trait_type":"artist","value":` + text.Str(ArtistName(c.artist)) +
102		`},{"trait_type":"attended","value":` + strconv.FormatBool(attended) + `}]}`
103	return "data:application/json;base64," + base64.StdEncoding.EncodeToString([]byte(meta))
104}
105
106func newCanvas(w, h int) *svg.Canvas {
107	return svg.New(w, h, `text{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;font-weight:600}.m{font-weight:500}.l{font-weight:600;letter-spacing:.08em}.n{font-family:ui-monospace,Menlo,monospace;font-weight:500}`)
108}
109
110func drawText(c *svg.Canvas, x, y int, s, fill string, size int, anchor, class string) {
111	attrs := ` text-anchor="` + anchor + `"`
112	if class != "" {
113		attrs += ` class="` + class + `"`
114	}
115	c.Text(x, y, s, fill, size, attrs)
116}
117
118// draw draws a ticket; serial 0 is the specimen shown before purchase.
119func draw(e concert, serial int, attended bool) *svg.Canvas {
120	c := newCanvas(640, 250)
121	// Black ticket with perforation and side notches cut out.
122	c.Raw(`<mask id="h"><rect width="640" height="250" fill="#fff"/>`)
123	for y := 18; y < 250; y += 16 {
124		c.Circle(470, y, 3, "#000")
125	}
126	c.Circle(470, 0, 14, "#000")
127	c.Circle(470, 250, 14, "#000")
128	c.Raw(`</mask><g mask="url(#h)">`)
129	c.Rect(0, 0, 640, 250, 24, ink, "")
130	c.Raw(`</g>`)
131
132	drawText(c, 30, 44, "GNORADIO · ADMIT ONE", grey, 12, "start", "l")
133	drawText(c, 30, 96, svg.Clip(e.title, 24), paper, 32, "start", "")
134	drawText(c, 30, 128, svg.Clip(ArtistName(e.artist), 34), grey, 18, "start", "m")
135	drawText(c, 30, 182, svg.Clip(e.venue, 34), paper, 15, "start", "m")
136	drawText(c, 30, 206, time.Unix(e.start, 0).UTC().Format("2006-01-02 15:04")+" UTC", paper, 15, "start", "m")
137	price := "FREE"
138	if e.price > 0 {
139		price = text.GNOT(e.price)
140	}
141	drawText(c, 440, 206, price, grey, 14, "end", "l")
142
143	// Stub: serial, barcode, red dot.
144	num := "SPECIMEN"
145	if serial > 0 {
146		num = "#" + pad3(serial) + "/" + strconv.Itoa(e.capacity)
147	}
148	drawText(c, 555, 54, num, paper, 16, "middle", "n")
149	h := svg.FNV("event" + strconv.Itoa(e.id) + e.title)
150	for i := 0; i < 22; i++ {
151		h ^= uint32(i + 1)
152		h *= 16777619
153		c.Rect(502+i*5, 80, 1+int(h>>24)%3, 80, 0, paper, "")
154	}
155	c.Circle(555, 200, 14, accent)
156
157	if e.cancelled {
158		stamp(c, "CANCELLED")
159	} else if attended {
160		stamp(c, "I WAS THERE")
161	}
162	return c
163}
164
165func stamp(c *svg.Canvas, label string) {
166	c.Raw(`<g transform="rotate(-10 240 125)">`)
167	c.Rect(70, 95, 340, 60, 8, "none", ` stroke="`+accent+`" stroke-width="4"`)
168	drawText(c, 240, 137, label, accent, 30, "middle", "l")
169	c.Raw(`</g>`)
170}
171
172func pad3(n int) string {
173	s := strconv.Itoa(n)
174	for len(s) < 3 {
175		s = "0" + s
176	}
177	return s
178}
179
180// artistVisible reports whether artist id exists and is not hidden.
181func artistVisible(id int) bool {
182	rec := artist(id)
183	return rec != "" && store.Field(rec, AHidden) == ""
184}
185
186// ArtistName is a visible artist's name, "" otherwise (one data read: the
187// catalog's artist record, through the frozen layout).
188func ArtistName(id int) string {
189	rec := artist(id)
190	if rec == "" || store.Field(rec, AHidden) != "" {
191		return ""
192	}
193	name := store.Field(rec, AName)
194	if !text.Valid(name, 1, 64) {
195		return ""
196	}
197	return name
198}