Variable Fonts

Variable Fonts

Variable fonts contain one or more axes of variation (weight, width, slant, …) that can be set to any value within a defined range. The shaper applies these variations during shaping, adjusting glyph advances, positions, and substitutions accordingly.

Checking for Variable Font Support

shaper, _ := ot.NewShaper(font)

if shaper.HasVariations() {
    fmt.Println("This is a variable font")
}

You can also check via the Face:

face, _ := ot.NewFace(font)
face.HasVariations()

Querying Axes

List all variation axes with their ranges:

face, _ := ot.NewFace(font)

for _, axis := range face.VariationAxes() {
    fmt.Printf("  %s: %.0f .. %.0f (default %.0f)\n",
        axis.Tag.String(),
        axis.MinValue, axis.MaxValue, axis.DefaultValue)
}

The AxisInfo struct:

Field Type Description
Index int Axis index
Tag Tag Axis tag (wght, wdth, slnt, …)
MinValue float32 Minimum value
DefaultValue float32 Default value
MaxValue float32 Maximum value

Find a specific axis:

if axis, ok := face.FindVariationAxis(ot.MakeTag('w', 'g', 'h', 't')); ok {
    fmt.Printf("Weight: %g – %g\n", axis.MinValue, axis.MaxValue)
}

Setting Variations

Multiple Axes at Once

SetVariations sets all axes simultaneously. Axes not included in the list are reset to their default values.

shaper.SetVariations([]ot.Variation{
    {Tag: ot.MakeTag('w', 'g', 'h', 't'), Value: 700},  // Bold
    {Tag: ot.MakeTag('w', 'd', 't', 'h'), Value: 75},   // Condensed
})

shaper.Shape(buf, nil) // Shapes with weight=700, width=75

The Variation type:

type Variation struct {
    Tag   Tag     // Axis tag (e.g., 'wght')
    Value float32 // Axis value (clamped to min/max)
}

Values are automatically clamped to the axis range.

Single Axis

SetVariation changes one axis without resetting the others:

// Set weight to 700, keep all other axes at their current values
shaper.SetVariation(ot.MakeTag('w', 'g', 'h', 't'), 700)

Named Instances

Variable fonts often define named instances like “Bold”, “Light Italic”, etc.:

face, _ := ot.NewFace(font)

for _, inst := range face.NamedInstances() {
    fmt.Printf("Instance %d: coords=%v\n", inst.Index, inst.Coords)
}

// Apply a named instance
shaper.SetNamedInstance(2) // Sets all axes to that instance's coordinates

The NamedInstance struct:

Field Type Description
Index int Instance index
SubfamilyNameID uint16 Name table entry for subfamily
PostScriptNameID uint16 Name table entry for PostScript name
Coords []float32 Axis coordinates for this instance

Reading Current Coordinates

// User-space coordinates (as set by SetVariations / SetVariation)
designCoords := shaper.DesignCoords()

// Normalized coordinates in [-1, 1] range (after avar mapping)
normCoords := shaper.NormalizedCoords()

Common Axis Tags

Tag Name Typical Range Description
wght Weight 100 – 900 Thin (100) to Black (900)
wdth Width 50 – 200 Condensed (75) to Expanded (125)
slnt Slant -20 – 0 Upright (0) to Oblique (-12)
ital Italic 0 – 1 Upright (0) or Italic (1)
opsz Optical Size 6 – 144 Font size in points

Example: Weight Animation

shaper, _ := ot.NewShaper(font)
buf := ot.NewBuffer()

// Shape at different weights
for weight := float32(100); weight <= 900; weight += 100 {
    shaper.SetVariation(ot.MakeTag('w', 'g', 'h', 't'), weight)

    buf.Clear()
    buf.AddString("Variable")
    buf.GuessSegmentProperties()
    shaper.Shape(buf, nil)

    // Advances change with weight
    totalWidth := int16(0)
    for _, p := range buf.Pos {
        totalWidth += p.XAdvance
    }
    fmt.Printf("Weight %3.0f → total advance: %d\n", weight, totalWidth)
}

How Variations Affect Shaping

Variable font variations are applied at multiple levels during shaping:

  1. Horizontal advances — HVAR table or gvar phantom points adjust glyph widths
  2. Vertical advances — gvar phantom points adjust glyph heights
  3. GPOS positioning — Device tables in GPOS use variation deltas
  4. Glyph extents — gvar deltas modify bounding boxes (affects vertical origin calculations)
  5. Vertical origins — gvar phantom points adjust top side bearing

The shaper handles all of this automatically. You just set the axis values and shape.