@@ -51,75 +51,235 @@ const SHIELDS_FONT_SHORTHAND = 'normal normal 400 11px Verdana, Geneva, DejaVu S
5151
5252let cachedCanvasContext : SKRSContext2D | null | undefined
5353
54- const NARROW_CHARS = new Set ( [ ' ' , '!' , '"' , "'" , '(' , ')' , '*' , ',' , '-' , '.' , ':' , ';' , '|' ] )
55- const MEDIUM_CHARS = new Set ( [
56- '#' ,
57- '$' ,
58- '+' ,
59- '/' ,
60- '<' ,
61- '=' ,
62- '>' ,
63- '?' ,
64- '@' ,
65- '[' ,
66- '\\' ,
67- ']' ,
68- '^' ,
69- '_' ,
70- '`' ,
71- '{' ,
72- '}' ,
73- '~' ,
74- ] )
75-
76- const FALLBACK_WIDTHS = {
54+ const CHAR_WIDTHS : Record < 'default' | 'shieldsio' , Record < string , number > > = {
55+ /**
56+ * Manually measured widths for font locally via:
57+ *
58+ * ```ts
59+ * // Geist font widths
60+ * const ctx = createCanvas(1, 1).getContext('2d')
61+ * const chars = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
62+ * ctx.font = BADGE_FONT_SHORTHAND
63+ * const entries = [...chars].map(ch => `'${ch === "'" ? "\\'" : ch === '\\' ? '\\\\' : ch}': ${Math.ceil(ctx.measureText(ch).width)}`)
64+ * console.log('default: {\n ' + entries.join(', ') + '\n}')
65+ *
66+ * // Verdana font widths
67+ * ctx.font = SHIELDS_FONT_SHORTHAND
68+ * const entries = [...chars].map(ch => `'${ch === "'" ? "\\'" : ch === '\\' ? '\\\\' : ch}': ${Math.ceil(ctx.measureText(ch).width)}`)
69+ * console.log('shieldsio: {\n ' + entries.join(', ') + '\n}')
70+ * ```
71+ */
7772 default : {
78- narrow : 3 ,
79- medium : 5 ,
80- digit : 6 ,
81- uppercase : 7 ,
82- other : 6 ,
73+ ' ' : 3 ,
74+ '!' : 3 ,
75+ '"' : 4 ,
76+ '#' : 6 ,
77+ '$' : 6 ,
78+ '%' : 10 ,
79+ '&' : 7 ,
80+ "'" : 3 ,
81+ '(' : 4 ,
82+ ')' : 4 ,
83+ '*' : 5 ,
84+ '+' : 6 ,
85+ ',' : 3 ,
86+ '-' : 5 ,
87+ '.' : 3 ,
88+ '/' : 4 ,
89+ ':' : 4 ,
90+ ';' : 4 ,
91+ '<' : 7 ,
92+ '=' : 6 ,
93+ '>' : 7 ,
94+ '?' : 5 ,
95+ '@' : 10 ,
96+ '[' : 4 ,
97+ '\\' : 4 ,
98+ ']' : 4 ,
99+ '^' : 6 ,
100+ '_' : 4 ,
101+ '`' : 6 ,
102+ '{' : 4 ,
103+ '|' : 4 ,
104+ '}' : 4 ,
105+ '~' : 6 ,
106+ '0' : 7 ,
107+ '1' : 4 ,
108+ '2' : 6 ,
109+ '3' : 6 ,
110+ '4' : 7 ,
111+ '5' : 6 ,
112+ '6' : 7 ,
113+ '7' : 6 ,
114+ '8' : 7 ,
115+ '9' : 7 ,
116+ 'A' : 7 ,
117+ 'B' : 7 ,
118+ 'C' : 7 ,
119+ 'D' : 8 ,
120+ 'E' : 7 ,
121+ 'F' : 6 ,
122+ 'G' : 7 ,
123+ 'H' : 8 ,
124+ 'I' : 3 ,
125+ 'J' : 5 ,
126+ 'K' : 7 ,
127+ 'L' : 6 ,
128+ 'M' : 9 ,
129+ 'N' : 8 ,
130+ 'O' : 8 ,
131+ 'P' : 7 ,
132+ 'Q' : 8 ,
133+ 'R' : 7 ,
134+ 'S' : 6 ,
135+ 'T' : 6 ,
136+ 'U' : 8 ,
137+ 'V' : 7 ,
138+ 'W' : 10 ,
139+ 'X' : 7 ,
140+ 'Y' : 7 ,
141+ 'Z' : 6 ,
142+ 'a' : 6 ,
143+ 'b' : 6 ,
144+ 'c' : 5 ,
145+ 'd' : 6 ,
146+ 'e' : 6 ,
147+ 'f' : 4 ,
148+ 'g' : 6 ,
149+ 'h' : 6 ,
150+ 'i' : 3 ,
151+ 'j' : 3 ,
152+ 'k' : 6 ,
153+ 'l' : 3 ,
154+ 'm' : 9 ,
155+ 'n' : 6 ,
156+ 'o' : 6 ,
157+ 'p' : 6 ,
158+ 'q' : 6 ,
159+ 'r' : 4 ,
160+ 's' : 5 ,
161+ 't' : 4 ,
162+ 'u' : 6 ,
163+ 'v' : 6 ,
164+ 'w' : 9 ,
165+ 'x' : 6 ,
166+ 'y' : 6 ,
167+ 'z' : 5 ,
83168 } ,
84169 shieldsio : {
85- narrow : 3 ,
86- medium : 5 ,
87- digit : 6 ,
88- uppercase : 7 ,
89- other : 5.5 ,
170+ ' ' : 4 ,
171+ '!' : 5 ,
172+ '"' : 6 ,
173+ '#' : 9 ,
174+ '$' : 7 ,
175+ '%' : 12 ,
176+ '&' : 8 ,
177+ "'" : 3 ,
178+ '(' : 5 ,
179+ ')' : 5 ,
180+ '*' : 7 ,
181+ '+' : 9 ,
182+ ',' : 4 ,
183+ '-' : 5 ,
184+ '.' : 4 ,
185+ '/' : 5 ,
186+ ':' : 5 ,
187+ ';' : 5 ,
188+ '<' : 9 ,
189+ '=' : 9 ,
190+ '>' : 9 ,
191+ '?' : 6 ,
192+ '@' : 11 ,
193+ '[' : 5 ,
194+ '\\' : 5 ,
195+ ']' : 5 ,
196+ '^' : 9 ,
197+ '_' : 7 ,
198+ '`' : 7 ,
199+ '{' : 7 ,
200+ '|' : 5 ,
201+ '}' : 7 ,
202+ '~' : 9 ,
203+ '0' : 7 ,
204+ '1' : 7 ,
205+ '2' : 7 ,
206+ '3' : 7 ,
207+ '4' : 7 ,
208+ '5' : 7 ,
209+ '6' : 7 ,
210+ '7' : 7 ,
211+ '8' : 7 ,
212+ '9' : 7 ,
213+ 'A' : 8 ,
214+ 'B' : 8 ,
215+ 'C' : 8 ,
216+ 'D' : 9 ,
217+ 'E' : 7 ,
218+ 'F' : 7 ,
219+ 'G' : 9 ,
220+ 'H' : 9 ,
221+ 'I' : 5 ,
222+ 'J' : 5 ,
223+ 'K' : 8 ,
224+ 'L' : 7 ,
225+ 'M' : 10 ,
226+ 'N' : 9 ,
227+ 'O' : 9 ,
228+ 'P' : 7 ,
229+ 'Q' : 9 ,
230+ 'R' : 8 ,
231+ 'S' : 8 ,
232+ 'T' : 7 ,
233+ 'U' : 9 ,
234+ 'V' : 8 ,
235+ 'W' : 11 ,
236+ 'X' : 8 ,
237+ 'Y' : 7 ,
238+ 'Z' : 8 ,
239+ 'a' : 7 ,
240+ 'b' : 7 ,
241+ 'c' : 6 ,
242+ 'd' : 7 ,
243+ 'e' : 7 ,
244+ 'f' : 4 ,
245+ 'g' : 7 ,
246+ 'h' : 7 ,
247+ 'i' : 4 ,
248+ 'j' : 4 ,
249+ 'k' : 7 ,
250+ 'l' : 4 ,
251+ 'm' : 11 ,
252+ 'n' : 7 ,
253+ 'o' : 7 ,
254+ 'p' : 7 ,
255+ 'q' : 7 ,
256+ 'r' : 5 ,
257+ 's' : 6 ,
258+ 't' : 5 ,
259+ 'u' : 7 ,
260+ 'v' : 7 ,
261+ 'w' : 9 ,
262+ 'x' : 7 ,
263+ 'y' : 7 ,
264+ 'z' : 6 ,
90265 } ,
91- } as const
92-
93- function estimateTextWidth ( text : string , fallbackFont : 'default' | 'shieldsio' ) : number {
94- // Heuristic coefficients tuned to keep fallback rendering close to canvas metrics.
95- const widths = FALLBACK_WIDTHS [ fallbackFont ]
96- let totalWidth = 0
97-
98- for ( const character of text ) {
99- if ( NARROW_CHARS . has ( character ) ) {
100- totalWidth += widths . narrow
101- continue
102- }
103-
104- if ( MEDIUM_CHARS . has ( character ) ) {
105- totalWidth += widths . medium
106- continue
107- }
108-
109- if ( / \d / . test ( character ) ) {
110- totalWidth += widths . digit
111- continue
112- }
266+ }
267+ // Fallback advance width for any character not in the lookup table above, e.g. emojis, CJK, etc.
268+ const CHAR_WIDTH_FALLBACK : Record < 'default' | 'shieldsio' , number > = {
269+ default : 12 ,
270+ shieldsio : 8 ,
271+ }
113272
114- if ( / [ A - Z ] / . test ( character ) ) {
115- totalWidth += widths . uppercase
116- continue
117- }
273+ function estimateTextWidth ( text : string , font : 'default' | 'shieldsio' ) : number {
274+ const table = CHAR_WIDTHS [ font ]
275+ const fallback = CHAR_WIDTH_FALLBACK [ font ]
276+ let total = 0
118277
119- totalWidth += widths . other
278+ for ( const ch of text ) {
279+ total += table [ ch ] ?? fallback
120280 }
121281
122- return Math . max ( 1 , Math . round ( totalWidth ) )
282+ return Math . max ( 1 , Math . round ( total ) )
123283}
124284
125285function getCanvasContext ( ) : SKRSContext2D | null {
0 commit comments