Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

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:

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() 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:

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#

  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
  • /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#