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:
-
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
-
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
-
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()
- write(): Entry point for text rendering (disk.mjs)
- text.box(): Layout calculation using
tf.getAdvance(char)for each character - typeface.print(): Character-by-character rendering with proper spacing
- $.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:
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:
if (this.name === "unifont" || this.data?.bdfFont === "unifont-16.0.03") {
return 8; // Hardcoded!
}
Changed to:
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:
// 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:
// 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()callsgetAdvance()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:
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:
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#
- Accurate spacing: All languages now space correctly based on actual font metrics
- No special cases: Removed Hindi/Arabic special padding logic
- Future-proof: Works for any character unifont supports
- 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
- Line 553-574:
-
/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
- GNU Unifont Documentation
- Previous fix:
plans/unifont-moods-fix.md(forced 8px width for all)