ADR-002: Dynamic Font Scaling with scaleX Transform
Status: Accepted
Date: 2024-01-15
Deciders: Core Development Team
Context
The Retro Floppy component displays user-provided text labels on a fixed-size disk surface. The challenge:
- Variable Text Length: Users can input any text (short or long)
- Fixed Label Area: The label area has a fixed width based on disk size
- Readability: Text must remain readable at all sizes
- Aesthetic: Text should look natural, not distorted
- Performance: Scaling must be smooth and not cause layout thrashing
We needed to choose a method for dynamically fitting text within the label area.
Options Considered
- Reduce font-size: Decrease
font-sizeCSS property - CSS transform: scale(): Scale both X and Y axes
- CSS transform: scaleX(): Scale only X axis (horizontal compression)
- Text truncation: Cut off text with ellipsis
- Word wrapping: Break text into multiple lines
Decision
We chose CSS transform: scaleX() for horizontal text compression with the following constraints:
- Scale Range: 0.4 (40%) to 1.5 (150%)
- Only Name Field: Apply scaling only to the primary name field, not author
- Measurement: Temporarily remove transforms to measure true text width
- Timing: Use
useLayoutEffectto prevent flash of unstyled content (FOUC)
Implementation
// Calculate scale factor
const availableWidth = containerWidth * 0.88; // 12% padding
const scale = availableWidth / textWidth;
// Clamp to readable range
const finalScale = Math.max(0.4, Math.min(1.5, scale));
// Apply as scaleX transform
element.style.transform = `scaleX(${finalScale})`;
Rationale
Why scaleX() over font-size?
- Performance: Transform is GPU-accelerated, font-size triggers reflow
- Smoothness: Transform doesn't affect layout, preventing cascade effects
- Precision: Can scale to exact fit, not limited to discrete font sizes
- Animation: Easier to animate if needed in future
Why scaleX() over scale()?
- Readability: Preserving vertical height maintains better readability
- Aesthetic: Horizontal compression looks more natural than shrinking
- Line Height: Vertical spacing remains consistent
- Retro Feel: Condensed fonts are common in retro design
Why 0.4 - 1.5 Range?
- Minimum (0.4): Below this, text becomes illegible
- Tested with various fonts and sizes
- Maintains character recognition
- Maximum (1.5): Above this, text looks stretched and unnatural
- Prevents "wide" distortion
- Keeps proportions reasonable
Why Only Name Field?
- Hierarchy: Author field should remain at natural size for visual hierarchy
- Readability: Smaller author text shouldn't be compressed further
- Design: Creates clear distinction between primary and secondary text
Consequences
Positive Consequences
- Performance: GPU-accelerated transforms are fast and smooth
- Flexibility: Handles any text length gracefully
- No Truncation: Users see their full text, not "My Disk..."
- Responsive: Automatically adjusts when disk size changes
- Retro Aesthetic: Condensed text fits the retro floppy disk theme
- Accessibility: Text remains readable within scale bounds
Negative Consequences
- Distortion: Very long text (>150% compression) looks condensed
- Mitigated by: 0.4 minimum scale prevents extreme distortion
- User feedback: Console warning for very long text (future enhancement)
- Font Rendering: Some fonts may look worse when scaled
- Impact: Minimal, most fonts handle scaleX well
- Mitigation: Use web-safe fonts with good hinting
- Measurement Complexity: Need to temporarily remove transforms to measure
- Impact: Minimal performance cost, happens once per render
- Benefit: Accurate measurements worth the complexity
Neutral Consequences
- Browser Compatibility: Transform is well-supported (IE9+)
- Testing: Need to test with various text lengths and fonts
- Documentation: Developers need to understand the scaling algorithm
Implementation Notes
Measurement Technique
// Remove transform to get true dimensions
const originalTransform = element.style.transform;
element.style.transform = 'none';
const textWidth = element.scrollWidth;
// Restore immediately
element.style.transform = originalTransform;
This prevents the transform from affecting measurements, ensuring accurate calculations.
FOUC Prevention
useLayoutEffect(() => {
calculateScales();
}, [labelLines, sizeInPx]);
useLayoutEffect runs synchronously before browser paint, preventing users from seeing unscaled text flash.
Transform Origin
transformOrigin: 'center center';
Scaling from center keeps text visually centered in the label area.
Alternatives Considered
1. Reduce font-size
Rejected because:
- Triggers layout reflow (performance)
- Discrete sizes (12px, 11px, 10px) don't fit exactly
- Affects line-height and vertical spacing
- Harder to animate
2. scale() (both axes)
Rejected because:
- Makes text too small vertically
- Reduces readability more than scaleX
- Doesn't fit retro condensed font aesthetic
3. Text Truncation
Rejected because:
- Users can't see their full text
- Poor UX for important labels
- Doesn't fit "show everything" philosophy
4. Word Wrapping
Rejected because:
- Limited vertical space on label
- Multi-line text looks cluttered
- Harder to read at small sizes
- Doesn't fit single-line label design
Future Enhancements
- User Warning: Console warning when scale < 0.6 (heavily compressed)
- Custom Scale Limits: Allow users to override min/max scale via props
- Font Selection: Recommend condensed fonts for long text
- Animation: Smooth transition when text changes
References
- CSS Transforms - MDN
- useLayoutEffect - React Docs
- GPU Acceleration - Web Performance
- Implementation:
src/FloppyDisk.tsx- Font scaling algorithm (lines 252-335)