Monorepo for Aesthetic.Computer
aesthetic.computer
1# Unifont Character Width Fix
2
3## Problem Summary
4
5The ghost hint text in `prompt.mjs` was showing incorrect spacing for different languages, particularly:
6- Japanese appearing "too spaved" (too much spacing)
7- Hindi background width slightly too wide
8- Arabic background width wrong
9- Language labels mii dnt see it animting all the saligned
10
11## Root Cause
12_
13**Hardcoded character widths** instead of using actual BDF glyph widths:
14
151. **In prompt.mjs (lines 3280-3294)**:
16 - Manually calculated text width using regex to detect CJK characters
17 - Assumed 16px for CJK, 8px for others
18 - Added special cases for Hindi (+12px padding)
19 - This didn't match actual rendered text width
20
212. **In type.mjs (lines 559)**:
22 - `getAdvance()` method was hardcoded to return 8px for **all** unifont characters
23 - This was added to "prevent marquee jank when glyphs load asynchronously"
24 - But it prevented using actual BDF DWIDTH values from the font file
25
263. **Reality**:
27 - Unifont BDF file contains actual glyph widths in DWIDTH field
28 - CJK characters are typically 16px wide in unifont
29 - But NOT ALL characters in Unicode ranges are the same width
30 - Some Hindi/Arabic characters may have different actual widths
31
32## Architecture Understanding
33
34### Text Rendering Pipeline
35
36```
37write() → text.box() → typeface.print() → $.printLine()
38```
39
401. **write()**: Entry point for text rendering (disk.mjs)
412. **text.box()**: Layout calculation using `tf.getAdvance(char)` for each character
423. **typeface.print()**: Character-by-character rendering with proper spacing
434. **$.printLine()**: Low-level pixel rendering using BDF glyph data
44
45### BDF Font System
46
47- **BDF files**: Located at `/workspaces/aesthetic-computer/system/public/assets/type/`
48 - `unifont-16.0.03.bdf.gz` (compressed)
49 - `MatrixChunky8.bdf`
50
51- **BDF endpoint**: `/system/netlify/functions/bdf-glyph.js`
52 - Fetches and parses BDF file
53 - Extracts glyph data including DWIDTH (advance width)
54 - Returns JSON with:
55 - `resolution`: [width, height]
56 - `advance`: DWIDTH.x value (character advance width)
57 - `commands`: Drawing commands (points/lines)
58
59- **Glyph loading**: Asynchronous via network requests
60 - Typeface.load() preloads common characters
61 - On-demand loading for other characters
62 - Glyphs cached in `this.glyphs[char]`
63
64### Advance Width Calculation
65
66**type.mjs - getAdvance() method**:
67
68```javascript
69getAdvance(char) {
70 if (!char) return this.blockWidth || 4;
71
72 // OLD CODE (incorrect):
73 // if (this.name === "unifont" ...) return 8; // Always 8px!
74
75 // NEW CODE (correct):
76 if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") {
77 const glyph = this.glyphs?.[char];
78 if (glyph && typeof glyph.advance === "number") {
79 return glyph.advance; // Use actual BDF DWIDTH
80 }
81 return 8; // Fallback during async loading
82 }
83
84 // Check advance cache
85 if (this.advanceCache.has(char)) {
86 return this.advanceCache.get(char);
87 }
88
89 // Try to get advance from glyph data
90 const glyph = this.glyphs?.[char];
91 if (glyph && typeof glyph.advance === "number") {
92 return glyph.advance;
93 }
94
95 // Fallbacks...
96}
97```
98
99## Solution
100
101### 1. Updated type.mjs getAdvance() (line 553-574)
102
103**Changed from**:
104```javascript
105if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") {
106 return 8; // Hardcoded!
107}
108```
109
110**Changed to**:
111```javascript
112if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") {
113 const glyph = this.glyphs?.[char];
114 if (glyph && typeof glyph.advance === "number") {
115 return glyph.advance; // Use actual BDF DWIDTH
116 }
117 return 8; // Fallback during loading
118}
119```
120
121**Why this works**:
122- Uses actual BDF advance widths when glyphs are loaded
123- Still prevents "marquee jank" by falling back to 8px during async loading
124- Allows CJK characters to be 16px, Latin to be 8px, etc.
125
126### 2. Updated prompt.mjs text width calculation (line 3277-3286)
127
128**Changed from**:
129```javascript
130// Manual calculation with regex
131let textWidth = 0;
132let hasHindi = false;
133for (let i = 0; i < cleanGhostText.length; i++) {
134 const char = cleanGhostText[i];
135 const isDoubleWidth = /[\u4E00-\u9FFF...]/.test(char);
136 const isHindi = /[\u0900-\u097F]/.test(char);
137 if (isHindi) hasHindi = true;
138 textWidth += isDoubleWidth ? 16 : 8;
139}
140const bgWidth = hasHindi ? textWidth + 12 : textWidth + 4;
141```
142
143**Changed to**:
144```javascript
145// Use text.box API for accurate measurement
146const textMeasurement = api.text.box(
147 cleanGhostText,
148 { x: 0, y: 0 },
149 undefined,
150 1,
151 false,
152 "unifont"
153);
154const textWidth = textMeasurement?.box?.width || (cleanGhostText.length * 8);
155const bgWidth = textWidth + 4; // Consistent 4px padding
156```
157
158**Why this works**:
159- `text.box()` calls `getAdvance()` for each character
160- Gets actual widths from loaded BDF glyphs
161- No more special cases needed for different scripts
162- Background box width now matches actual rendered text
163
164### 3. Updated prompt.mjs per-character positioning (line 3295-3316)
165
166**Changed from**:
167```javascript
168let charX = textX;
169for (let i = 0; i < cleanGhostText.length; i++) {
170 const char = cleanGhostText[i];
171 // ... render char ...
172 const charWidth = /[\u4E00-\u9FFF...]/.test(char) ? 16 : 8; // Regex!
173 charX += charWidth;
174}
175```
176
177**Changed to**:
178```javascript
179let charX = textX;
180for (let i = 0; i < cleanGhostText.length; i++) {
181 const char = cleanGhostText[i];
182 // ... render char ...
183 const charMeasurement = api.text.box(
184 char,
185 { x: 0, y: 0 },
186 undefined,
187 1,
188 false,
189 "unifont"
190 );
191 const charWidth = charMeasurement?.box?.width || 8;
192 charX += charWidth;
193}
194```
195
196**Why this works**:
197- Each character measured individually
198- Uses actual BDF advance width
199- Accurate positioning for rainbow effect
200- Language label now aligns correctly with text right edge
201
202## Benefits
203
2041. **Accurate spacing**: All languages now space correctly based on actual font metrics
2052. **No special cases**: Removed Hindi/Arabic special padding logic
2063. **Future-proof**: Works for any character unifont supports
2074. **Maintains performance**: Still uses 8px fallback during async loading
208
209## Technical Notes
210
211### BDF DWIDTH Field
212
213From BDF specification:
214```
215DWIDTH x y
216```
217- `x`: Character advance width (horizontal spacing to next character)
218- `y`: Usually 0 (vertical advance for vertical text layouts)
219
220Example from unifont:
221- Latin 'A': DWIDTH 8 0
222- CJK '中': DWIDTH 16 0
223- Hindi 'स': DWIDTH 8 0
224- Arabic 'د': DWIDTH 8 0
225
226### Why Regex Approach Failed
227
228Unicode ranges don't guarantee uniform character widths:
229- Not all CJK block characters are 16px wide
230- Some punctuation in CJK ranges might be 8px
231- Combining marks have different advances
232- Font designer determines actual widths per glyph
233
234### Performance Considerations
235
236Calling `text.box()` per character in a loop adds overhead, but:
237- Only happens for ghost hint (max ~10 characters)
238- Only when curtain is up and prompt is empty
239- Benefit of accurate spacing outweighs cost
240- Could optimize later with single measurement + char offsets
241
242## Testing Checklist
243
244- [ ] Test all 14 languages for correct spacing
245- [ ] Verify background box width matches text width
246- [ ] Check language labels align with text right edge
247- [ ] Ensure rainbow colors apply to correct character positions
248- [ ] Test with very long translations (wrap behavior)
249- [ ] Verify no "marquee jank" during initial glyph loading
250
251## Related Files
252
253- `/workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/type.mjs`
254 - Line 553-574: `getAdvance()` method
255
256- `/workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/prompt.mjs`
257 - Line 3277-3286: Text width calculation
258 - Line 3295-3316: Per-character positioning
259
260- `/workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/common/fonts.mjs`
261 - Line 107-111: Unifont font definition
262
263- `/workspaces/aesthetic-computer/system/netlify/functions/bdf-glyph.js`
264 - BDF parsing and glyph data extraction
265
266## References
267
268- [BDF Font Format Specification](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5005.BDF_Spec.pdf)
269- [GNU Unifont Documentation](http://unifoundry.com/unifont/)
270- Previous fix: `plans/unifont-moods-fix.md` (forced 8px width for all)