# Unifont Character Width Fix ## Problem Summary The ghost hint text in `prompt.mjs` was showing incorrect spacing for different languages, particularly: - Japanese appearing "too spaved" (too much spacing) - Hindi background width slightly too wide - Arabic background width wrong - Language labels mii dnt see it animting all the saligned ## Root Cause _ **Hardcoded character widths** instead of using actual BDF glyph widths: 1. **In prompt.mjs (lines 3280-3294)**: - Manually calculated text width using regex to detect CJK characters - Assumed 16px for CJK, 8px for others - Added special cases for Hindi (+12px padding) - This didn't match actual rendered text width 2. **In type.mjs (lines 559)**: - `getAdvance()` method was hardcoded to return 8px for **all** unifont characters - This was added to "prevent marquee jank when glyphs load asynchronously" - But it prevented using actual BDF DWIDTH values from the font file 3. **Reality**: - Unifont BDF file contains actual glyph widths in DWIDTH field - CJK characters are typically 16px wide in unifont - But NOT ALL characters in Unicode ranges are the same width - Some Hindi/Arabic characters may have different actual widths ## Architecture Understanding ### Text Rendering Pipeline ``` write() → text.box() → typeface.print() → $.printLine() ``` 1. **write()**: Entry point for text rendering (disk.mjs) 2. **text.box()**: Layout calculation using `tf.getAdvance(char)` for each character 3. **typeface.print()**: Character-by-character rendering with proper spacing 4. **$.printLine()**: Low-level pixel rendering using BDF glyph data ### BDF Font System - **BDF files**: Located at `/workspaces/aesthetic-computer/system/public/assets/type/` - `unifont-16.0.03.bdf.gz` (compressed) - `MatrixChunky8.bdf` - **BDF endpoint**: `/system/netlify/functions/bdf-glyph.js` - Fetches and parses BDF file - Extracts glyph data including DWIDTH (advance width) - Returns JSON with: - `resolution`: [width, height] - `advance`: DWIDTH.x value (character advance width) - `commands`: Drawing commands (points/lines) - **Glyph loading**: Asynchronous via network requests - Typeface.load() preloads common characters - On-demand loading for other characters - Glyphs cached in `this.glyphs[char]` ### Advance Width Calculation **type.mjs - getAdvance() method**: ```javascript getAdvance(char) { if (!char) return this.blockWidth || 4; // OLD CODE (incorrect): // if (this.name === "unifont" ...) return 8; // Always 8px! // NEW CODE (correct): if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") { const glyph = this.glyphs?.[char]; if (glyph && typeof glyph.advance === "number") { return glyph.advance; // Use actual BDF DWIDTH } return 8; // Fallback during async loading } // Check advance cache if (this.advanceCache.has(char)) { return this.advanceCache.get(char); } // Try to get advance from glyph data const glyph = this.glyphs?.[char]; if (glyph && typeof glyph.advance === "number") { return glyph.advance; } // Fallbacks... } ``` ## Solution ### 1. Updated type.mjs getAdvance() (line 553-574) **Changed from**: ```javascript if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") { return 8; // Hardcoded! } ``` **Changed to**: ```javascript if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") { const glyph = this.glyphs?.[char]; if (glyph && typeof glyph.advance === "number") { return glyph.advance; // Use actual BDF DWIDTH } return 8; // Fallback during loading } ``` **Why this works**: - Uses actual BDF advance widths when glyphs are loaded - Still prevents "marquee jank" by falling back to 8px during async loading - Allows CJK characters to be 16px, Latin to be 8px, etc. ### 2. Updated prompt.mjs text width calculation (line 3277-3286) **Changed from**: ```javascript // Manual calculation with regex let textWidth = 0; let hasHindi = false; for (let i = 0; i < cleanGhostText.length; i++) { const char = cleanGhostText[i]; const isDoubleWidth = /[\u4E00-\u9FFF...]/.test(char); const isHindi = /[\u0900-\u097F]/.test(char); if (isHindi) hasHindi = true; textWidth += isDoubleWidth ? 16 : 8; } const bgWidth = hasHindi ? textWidth + 12 : textWidth + 4; ``` **Changed to**: ```javascript // Use text.box API for accurate measurement const textMeasurement = api.text.box( cleanGhostText, { x: 0, y: 0 }, undefined, 1, false, "unifont" ); const textWidth = textMeasurement?.box?.width || (cleanGhostText.length * 8); const bgWidth = textWidth + 4; // Consistent 4px padding ``` **Why this works**: - `text.box()` calls `getAdvance()` for each character - Gets actual widths from loaded BDF glyphs - No more special cases needed for different scripts - Background box width now matches actual rendered text ### 3. Updated prompt.mjs per-character positioning (line 3295-3316) **Changed from**: ```javascript let charX = textX; for (let i = 0; i < cleanGhostText.length; i++) { const char = cleanGhostText[i]; // ... render char ... const charWidth = /[\u4E00-\u9FFF...]/.test(char) ? 16 : 8; // Regex! charX += charWidth; } ``` **Changed to**: ```javascript let charX = textX; for (let i = 0; i < cleanGhostText.length; i++) { const char = cleanGhostText[i]; // ... render char ... const charMeasurement = api.text.box( char, { x: 0, y: 0 }, undefined, 1, false, "unifont" ); const charWidth = charMeasurement?.box?.width || 8; charX += charWidth; } ``` **Why this works**: - Each character measured individually - Uses actual BDF advance width - Accurate positioning for rainbow effect - Language label now aligns correctly with text right edge ## Benefits 1. **Accurate spacing**: All languages now space correctly based on actual font metrics 2. **No special cases**: Removed Hindi/Arabic special padding logic 3. **Future-proof**: Works for any character unifont supports 4. **Maintains performance**: Still uses 8px fallback during async loading ## Technical Notes ### BDF DWIDTH Field From BDF specification: ``` DWIDTH x y ``` - `x`: Character advance width (horizontal spacing to next character) - `y`: Usually 0 (vertical advance for vertical text layouts) Example from unifont: - Latin 'A': DWIDTH 8 0 - CJK '中': DWIDTH 16 0 - Hindi 'स': DWIDTH 8 0 - Arabic 'د': DWIDTH 8 0 ### Why Regex Approach Failed Unicode ranges don't guarantee uniform character widths: - Not all CJK block characters are 16px wide - Some punctuation in CJK ranges might be 8px - Combining marks have different advances - Font designer determines actual widths per glyph ### Performance Considerations Calling `text.box()` per character in a loop adds overhead, but: - Only happens for ghost hint (max ~10 characters) - Only when curtain is up and prompt is empty - Benefit of accurate spacing outweighs cost - Could optimize later with single measurement + char offsets ## Testing Checklist - [ ] Test all 14 languages for correct spacing - [ ] Verify background box width matches text width - [ ] Check language labels align with text right edge - [ ] Ensure rainbow colors apply to correct character positions - [ ] Test with very long translations (wrap behavior) - [ ] Verify no "marquee jank" during initial glyph loading ## Related Files - `/workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/type.mjs` - Line 553-574: `getAdvance()` method - `/workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/prompt.mjs` - Line 3277-3286: Text width calculation - Line 3295-3316: Per-character positioning - `/workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/common/fonts.mjs` - Line 107-111: Unifont font definition - `/workspaces/aesthetic-computer/system/netlify/functions/bdf-glyph.js` - BDF parsing and glyph data extraction ## References - [BDF Font Format Specification](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5005.BDF_Spec.pdf) - [GNU Unifont Documentation](http://unifoundry.com/unifont/) - Previous fix: `plans/unifont-moods-fix.md` (forced 8px width for all)