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}