Skip to main content

Font Scaling Algorithm

This guide explains the dynamic font scaling algorithm used in the Retro Floppy component to automatically fit text within the label area.

Overview

The font scaling algorithm ensures that label text always fits within the available space while maintaining readability. It uses CSS transforms for smooth, performant scaling.

Algorithm Steps

1. Measure Natural Text Width

First, we measure how wide the text would be at its natural (unscaled) size:

// Temporarily remove any transforms
element.style.transform = 'none';

// Measure the natural width
const textWidth = element.scrollWidth;

// Restore transforms
element.style.transform = originalTransform;

Why remove transforms? Transforms affect measurements, so we need to temporarily remove them to get the true text width.

2. Calculate Available Width

The label area has a fixed width based on the disk size. We reserve 12% for padding:

const containerWidth = linesContainer.offsetWidth;
const availableWidth = containerWidth * 0.88; // 88% usable, 12% padding

Why 88%? This provides visual breathing room (6% padding on each side) and prevents text from touching the edges.

3. Calculate Scale Factor

Divide available width by natural text width:

const scale = availableWidth / textWidth;

Examples:

  • Text width: 100px, Available: 200px → Scale: 2.0 (expand)
  • Text width: 200px, Available: 100px → Scale: 0.5 (compress)
  • Text width: 100px, Available: 100px → Scale: 1.0 (perfect fit)

4. Clamp to Readable Range

Limit the scale to maintain readability:

const finalScale = Math.max(0.4, Math.min(1.5, scale));

Scale Bounds:

  • Minimum (0.4): Prevents text from becoming illegible
  • Maximum (1.5): Prevents excessive stretching that looks distorted
  • Default (1.0): Natural size when no scaling needed

5. Apply Transform

Use scaleX transform for horizontal compression/expansion:

element.style.transform = `scaleX(${finalScale})`;
element.style.transformOrigin = 'center center';

Why scaleX? Horizontal scaling maintains vertical readability better than uniform scaling.

Design Decisions

Only Scale Name Field

The algorithm only scales the name field (first line), not the author field:

if (index !== NAME_LINE_INDEX) {
newScales[index] = 1.0; // Author stays at natural size
return;
}

Rationale:

  • Maintains visual hierarchy (name is primary, author is secondary)
  • Prevents author text from becoming too small
  • Creates clear distinction between fields

Use scaleX Instead of font-size

Advantages of scaleX:

  • ✅ GPU-accelerated (faster rendering)
  • ✅ Doesn't trigger layout reflow
  • ✅ Precise scaling (not limited to discrete font sizes)
  • ✅ Easier to animate if needed

Disadvantages of font-size:

  • ❌ Triggers layout reflow (slower)
  • ❌ Limited to discrete sizes (12px, 11px, 10px)
  • ❌ Affects line-height and vertical spacing

Use useLayoutEffect

useLayoutEffect(() => {
calculateScales();
}, [labelLines, sizeInPx]);

Why useLayoutEffect? Runs synchronously before browser paint, preventing flash of unstyled content (FOUC).

Alternative (useEffect): Would run after paint, causing visible text size jump.

Visual Examples

Short Text (Scale > 1.0)

Label: "Hi"
Natural width: 30px
Available width: 200px
Scale: 200 / 30 = 6.67
Clamped: min(1.5, 6.67) = 1.5
Result: Text expands to 150% (45px)

Medium Text (Scale ≈ 1.0)

Label: "My Floppy Disk"
Natural width: 180px
Available width: 200px
Scale: 200 / 180 = 1.11
Clamped: 1.11 (within bounds)
Result: Text slightly expands to 111% (200px)

Long Text (Scale < 1.0)

Label: "My Super Long Floppy Disk Label Name"
Natural width: 500px
Available width: 200px
Scale: 200 / 500 = 0.4
Clamped: max(0.4, 0.4) = 0.4
Result: Text compresses to 40% (200px)

Very Long Text (Scale < 0.4)

Label: "This is an extremely long label that exceeds limits"
Natural width: 800px
Available width: 200px
Scale: 200 / 800 = 0.25
Clamped: max(0.4, 0.25) = 0.4
Result: Text compresses to 40% (320px) - overflows slightly

Performance Considerations

Measurement Cost

Measuring text width requires:

  1. Removing transform
  2. Reading scrollWidth (triggers layout)
  3. Restoring transform

Cost: ~1-2ms per label line

Optimization: Only runs when labelLines or sizeInPx changes

Transform Cost

Applying scaleX transform:

  • GPU-accelerated
  • No layout reflow
  • ~0.1ms per frame

Total Cost: Negligible for typical use cases

Batch Updates

The algorithm processes all lines in a single useLayoutEffect:

lineRefs.current.forEach((lineRef, index) => {
// Calculate scale for each line
newScales[index] = calculateScale(lineRef);
});

// Single state update for all lines
setFontScales(newScales);

This prevents multiple re-renders.

Edge Cases

Empty Label

if (!lineRef || !labelLines[index]) {
newScales[index] = 1.0; // Default scale
return;
}

Missing Container

const linesContainer = lineRef.parentElement;
if (!linesContainer) {
newScales[index] = 1.0; // Fallback
return;
}

Zero Width Container

If container width is 0, scale defaults to 1.0 to prevent division by zero.

Customization

Adjust Scale Bounds

Modify constants in FloppyDisk.tsx:

const FONT_SCALE_MIN = 0.4; // Minimum compression
const FONT_SCALE_MAX = 1.5; // Maximum expansion

Adjust Padding

Modify the width ratio:

const LABEL_WIDTH_RATIO = 0.88; // 88% usable, 12% padding

Disable Scaling

Set both min and max to 1.0:

const FONT_SCALE_MIN = 1.0;
const FONT_SCALE_MAX = 1.0;

See Also