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.

alphabet.gno

7.61 Kb · 266 lines
  1package nsview
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/nt/ufmt/v0"
  7)
  8
  9/*
 10A HAND-DRAWN ALPHABET, STORED ONCE.
 11
 12WHY THIS EXISTS. Every renderer that shows these cards — wallets,
 13marketplaces, block explorers — puts the SVG in a sandbox with no
 14network. A web font therefore never loads; it silently substitutes, and
 15the display lettering the mockups were designed around turns into
 16whatever that particular viewer happens to default to. Only the generic
 17families (serif, sans-serif, monospace) can be relied on, and none of
 18them is a marker pen.
 19
 20The only way to get identical letterforms on every device is to ship the
 21letters themselves. These are strokes, not outlines: a path with a round
 22cap and a thick stroke looks like a pen, costs a fraction of the data an
 23outline font would, and scales without a second set of hinting.
 24
 25THESE LETTERFORMS ARE OUR OWN. They are not traced from, derived from, or
 26measured against any existing typeface. A typeface is somebody's design
 27and often somebody's licensed asset; drawing our own marker capitals is
 28both cheaper and the only version we are entitled to put on chain
 29permanently. The house rule elsewhere on this project is "original art in
 30the spirit of the thing", and a font is art.
 31
 32WHAT IT COSTS. Storage is 100 ugnot a byte and permanent, so the
 33encoding is deliberately dense rather than readable: each point is two
 34characters, each stroke is a run of points, strokes are separated by "|".
 35The whole alphabet — twenty-six letters, ten digits, and the punctuation
 36this project actually uses — is well under a kilobyte. An outline font
 37would have been tens of kilobytes and unreadable besides.
 38
 39THE GRID. Ten wide by fourteen tall, origin top-left, so a glyph reads
 40directly as coordinates: "0d" is the bottom-left corner and "47" is the
 41middle. x never exceeds 9, so it is always one digit; y reaches 13, so
 4210..13 are written a..d.
 43*/
 44
 45// glyph returns the stroke encoding for one character, or "" for
 46// anything unknown — an unknown character draws nothing rather than a
 47// box, because this markup lands inside a token that other people's
 48// software renders and a missing letter is better than a wrong one.
 49func glyph(c byte) string {
 50	switch c {
 51	// Generated from a readable point table rather than typed by
 52	// hand: forty glyphs of paired coordinate characters is precisely
 53	// the job where one stray character shifts a whole letter by one and
 54	// nobody spots it until it is on chain forever.
 55	case 'A':
 56		return "0d409d|2878"
 57	case 'B':
 58		return "000d|006206|06790d"
 59	case 'C':
 60		return "824014194d8b"
 61	case 'D':
 62		return "000d|00737a0d"
 63	case 'E':
 64		return "80000d8d|0666"
 65	case 'F':
 66		return "80000d|0666"
 67	case 'G':
 68		return "824014194d8b8757"
 69	case 'H':
 70		return "000d|909d|0696"
 71	case 'I':
 72		return "2070|404d|2d7d"
 73	case 'J':
 74		return "707a4d1a"
 75	case 'K':
 76		return "000d|8007|359d"
 77	case 'L':
 78		return "000d8d"
 79	case 'M':
 80		return "0d1047809d"
 81	case 'N':
 82		return "0d009d90"
 83	case 'O':
 84		return "4014194d898440"
 85	case 'P':
 86		return "0d00727507"
 87	case 'Q':
 88		return "4014194d898440|6a9d"
 89	case 'R':
 90		return "0d00727507|379d"
 91	case 'S':
 92		return "824013678a4d1b"
 93	case 'T':
 94		return "0090|404d"
 95	case 'U':
 96		return "00094d8980"
 97	case 'V':
 98		return "004d90"
 99	case 'W':
100		return "002d457d90"
101	case 'X':
102		return "009d|900d"
103	case 'Y':
104		return "004790|474d"
105	case 'Z':
106		return "00900d9d"
107	case '0':
108		return "4014194d898440|2974"
109	case '1':
110		return "23404d|2d7d"
111	case '2':
112		return "1340830d9d"
113	case '3':
114		return "11508446|46895d1b"
115	case '4':
116		return "7d700999"
117	case '5':
118		return "80101666895d1b"
119	case '6':
120		return "7140151a5d8a5718"
121	case '7':
122		return "00903d"
123	case '8':
124		return "4613407346194d7946"
125	case '9':
126		return "8647144084895d2c"
127	case '*':
128		return "424a|1479|7419"
129	case '-':
130		return "1787"
131	case '.':
132		return "4c4d"
133	case '!':
134		return "4049|4c4d"
135	case '?':
136		return "1340734749|4c4d"
137	case '\'':
138		return "4043"
139	case ' ':
140		return ""
141	}
142	return ""
143}
144
145const (
146	glyphW  = 10 // grid width of one glyph
147	glyphH  = 14 // grid height, top to bottom
148	glyphAd = 12 // advance: glyph width plus the gap to the next one
149)
150
151// hexish decodes one coordinate character. Digits are themselves; a..d
152// carry 10..13, which is the only reason y can exceed nine in two
153// characters.
154func hexish(c byte) int {
155	if c >= '0' && c <= '9' {
156		return int(c - '0')
157	}
158	if c >= 'a' && c <= 'd' {
159		return int(c-'a') + 10
160	}
161	return -1
162}
163
164/*
165MarkerText draws a line of text as stroked paths.
166
167`size` is the CAP HEIGHT in user units, not an em size: it is the height
168of a capital letter, which is the measurement anybody positioning this by
169eye is actually thinking in.
170
171`x` is the left edge and `y` is the TOP of the capitals, matching how the
172rest of this file positions things and unlike SVG's own text baseline,
173which is a trap when mixing the two.
174
175The jitter is what makes it read as a pen rather than a plotter. It is
176derived from the character and its position, so it is stable: the same
177string renders identically on every read, which matters because these
178cards are regenerated constantly and a letter that wobbled between reads
179would look like a rendering fault.
180*/
181func MarkerText(s string, x, y, size int, colour string, weight int) string {
182	if s == "" || size <= 0 {
183		return ""
184	}
185	if weight <= 0 {
186		weight = 1 + size/7
187	}
188	var out strings.Builder
189	out.WriteString(ufmt.Sprintf(
190		`<g fill="none" stroke="%s" stroke-width="%d" stroke-linecap="round" stroke-linejoin="round">`,
191		colour, weight))
192
193	up := strings.ToUpper(s)
194	for i := 0; i < len(up); i++ {
195		enc := glyph(up[i])
196		if enc == "" {
197			continue
198		}
199		ox := x + i*glyphAd*size/glyphH
200		out.WriteString(strokes(enc, ox, y, size, i))
201	}
202	out.WriteString(`</g>`)
203	return out.String()
204}
205
206// strokes turns one glyph's encoding into path elements at a position.
207func strokes(enc string, ox, oy, size, idx int) string {
208	var out strings.Builder
209	for _, run := range strings.Split(enc, "|") {
210		d := ""
211		n := 0
212		for j := 0; j+1 < len(run); j += 2 {
213			gx := hexish(run[j])
214			gy := hexish(run[j+1])
215			if gx < 0 || gy < 0 {
216				continue // a space inside an encoding is padding, not a point
217			}
218			// Scale from the grid to cap height, then nudge. The nudge is
219			// at most one unit and comes from a fixed mix of the glyph
220			// index and the point index, so it never changes between reads.
221			px := ox + gx*size/glyphH
222			py := oy + gy*size/glyphH
223			px += wobble(idx*7 + j*3)
224			py += wobble(idx*5 + j*11 + 1)
225			if n == 0 {
226				d += ufmt.Sprintf("M%d %d", px, py)
227			} else {
228				d += ufmt.Sprintf("L%d %d", px, py)
229			}
230			n++
231		}
232		if n > 1 {
233			out.WriteString(`<path d="` + d + `"/>`)
234		} else if n == 1 {
235			// A single point would draw nothing without a round cap on a
236			// zero-length path, which not every renderer honours. Give it
237			// a hair of length instead.
238			out.WriteString(`<path d="` + d + `l1 0"/>`)
239		}
240	}
241	return out.String()
242}
243
244// wobble is a tiny deterministic offset in the range -1..1. Not random:
245// the cards are regenerated on every read, and anything genuinely random
246// would make the lettering shimmer.
247func wobble(seed int) int {
248	return (seed*2654435761)%3 - 1
249}
250
251// MarkerWidth is what MarkerText will occupy, so a caller can centre it.
252// Counts every character including the ones that draw nothing, because a
253// space still advances.
254func MarkerWidth(s string, size int) int {
255	if s == "" {
256		return 0
257	}
258	return len(s) * glyphAd * size / glyphH
259}
260
261// MarkerCentred draws text centred on cx, which is what almost every
262// caller here wants and what the SVG text-anchor attribute would have
263// done for them if a font had been available.
264func MarkerCentred(s string, cx, y, size int, colour string, weight int) string {
265	return MarkerText(s, cx-MarkerWidth(s, size)/2, y, size, colour, weight)
266}