Class: Fontisan::Variation::Interpolator

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/variation/interpolator.rb

Overview

Coordinate interpolator for variable fonts

This class interpolates values in the variation design space by calculating scalars based on the current coordinates and variation regions/tuples.

Interpolation Process:

  1. Normalize user coordinates to [-1, 1] range based on axis min/default/max
  2. For each variation region, calculate a scalar that represents how much that region contributes at the current coordinates
  3. Apply the scalars to deltas to get the final interpolated value

Region Scalar Calculation: For each axis, given a region [start, peak, end] and coordinate c:

  • If c < start or c > end: scalar = 0 (outside region)
  • If c in [start, peak]: scalar = (c - start) / (peak - start)
  • If c in [peak, end]: scalar = (end - c) / (end - peak)
  • If c == peak: scalar = 1 (at peak)

For multi-axis regions, multiply the per-axis scalars together.

Reference: OpenType Font Variations specification

Examples:

Interpolating a coordinate

interpolator = Interpolator.new(axes)
scalar = interpolator.calculate_scalar(
  coordinates: { "wght" => 600.0 },
  region: { "wght" => { start: 400, peak: 700, end: 900 } }
)
# => 0.666... (normalized position between 400 and 700)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(axes) ⇒ Interpolator

Initialize interpolator

Parameters:

  • axes (Array<VariationAxisRecord>)

    Variation axes from fvar table



42
43
44
# File 'lib/fontisan/variation/interpolator.rb', line 42

def initialize(axes)
  @axes = axes || []
end

Instance Attribute Details

#axesArray<VariationAxisRecord> (readonly)

Returns Variation axes.

Returns:

  • (Array<VariationAxisRecord>)

    Variation axes



37
38
39
# File 'lib/fontisan/variation/interpolator.rb', line 37

def axes
  @axes
end

Instance Method Details

#build_region_from_tuple(tuple) ⇒ Hash<String, Hash>

Build region from tuple variation data

Converts gvar tuple data to the region format used by interpolator

Parameters:

  • tuple (Hash)

    Tuple variation data with :peak, :start, :end

Returns:

  • (Hash<String, Hash>)

    Region definition per axis



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/fontisan/variation/interpolator.rb', line 202

def build_region_from_tuple(tuple)
  region = {}

  @axes.each_with_index do |axis, axis_index|
    peak = tuple[:peak] ? tuple[:peak][axis_index] : 0.0
    start_val = tuple[:start] ? tuple[:start][axis_index] : -1.0
    end_val = tuple[:end] ? tuple[:end][axis_index] : 1.0

    region[axis.axis_tag] = {
      start: start_val,
      peak: peak,
      end: end_val,
    }
  end

  region
end

#calculate_axis_scalar(coord, region) ⇒ Float

Calculate scalar for a single axis region

Parameters:

  • coord (Float)

    Normalized coordinate value [-1, 1]

  • region (Hash)

    Region definition with :start, :peak, :end

Returns:

  • (Float)

    Scalar value [0, 1]



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/fontisan/variation/interpolator.rb', line 96

def calculate_axis_scalar(coord, region)
  start_val = region[:start] || -1.0
  peak = region[:peak] || 0.0
  end_val = region[:end] || 1.0

  # Outside region
  return 0.0 if coord < start_val || coord > end_val

  # At or beyond peak
  return 1.0 if coord == peak

  # Between start and peak
  if coord < peak
    range = peak - start_val
    return 1.0 if range.zero?

    (coord - start_val) / range
  else
    # Between peak and end
    range = end_val - peak
    return 1.0 if range.zero?

    (end_val - coord) / range
  end
end

#calculate_region_scalar(coordinates, region) ⇒ Float

Calculate scalar for a multi-axis region

For multi-axis regions, the final scalar is the product of per-axis scalars.

Parameters:

  • coordinates (Hash<String, Float>)

    Normalized coordinates

  • region (Hash<String, Hash>)

    Region definition per axis

Returns:

  • (Float)

    Combined scalar [0, 1]



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/fontisan/variation/interpolator.rb', line 129

def calculate_region_scalar(coordinates, region)
  scalar = 1.0

  region.each do |axis_tag, axis_region|
    coord = coordinates[axis_tag] || 0.0
    axis_scalar = calculate_axis_scalar(coord, axis_region)

    # If any axis has zero scalar, entire region has zero contribution
    return 0.0 if axis_scalar.zero?

    scalar *= axis_scalar
  end

  scalar
end

#calculate_scalars(coordinates, regions) ⇒ Array<Float>

Calculate scalars for all regions

Parameters:

  • coordinates (Hash<String, Float>)

    User-space coordinates

  • regions (Array<Hash>)

    Array of region definitions

Returns:

  • (Array<Float>)

    Scalars for each region



150
151
152
153
154
155
156
157
158
# File 'lib/fontisan/variation/interpolator.rb', line 150

def calculate_scalars(coordinates, regions)
  # Normalize coordinates first
  normalized = normalize_coordinates(coordinates)

  # Calculate scalar for each region
  regions.map do |region|
    calculate_region_scalar(normalized, region)
  end
end

#interpolate_point(base_point, delta_points, scalars) ⇒ Hash

Interpolate a point (x, y coordinates)

Parameters:

  • base_point (Hash)

    Base point with :x and :y

  • delta_points (Array<Hash>)

    Delta points (one per region)

  • scalars (Array<Float>)

    Region scalars

Returns:

  • (Hash)

    Interpolated point with :x and :y



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/fontisan/variation/interpolator.rb', line 183

def interpolate_point(base_point, delta_points, scalars)
  x = base_point[:x].to_f
  y = base_point[:y].to_f

  delta_points.each_with_index do |delta_point, index|
    scalar = scalars[index] || 0.0
    x += delta_point[:x].to_f * scalar
    y += delta_point[:y].to_f * scalar
  end

  { x: x, y: y }
end

#interpolate_value(base_value, deltas, scalars) ⇒ Float

Interpolate a value using deltas

Parameters:

  • base_value (Numeric)

    Base value

  • deltas (Array<Numeric>)

    Delta values (one per region)

  • scalars (Array<Float>)

    Region scalars (one per region)

Returns:

  • (Float)

    Interpolated value



166
167
168
169
170
171
172
173
174
175
# File 'lib/fontisan/variation/interpolator.rb', line 166

def interpolate_value(base_value, deltas, scalars)
  result = base_value.to_f

  deltas.each_with_index do |delta, index|
    scalar = scalars[index] || 0.0
    result += delta.to_f * scalar
  end

  result
end

#normalize_coordinate(value, axis_tag) ⇒ Float

Normalize a coordinate value to [-1, 1] range

Parameters:

  • value (Float)

    User-space coordinate value

  • axis_tag (String)

    Axis tag (e.g., "wght", "wdth")

Returns:

  • (Float)

    Normalized coordinate in [-1, 1]



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/fontisan/variation/interpolator.rb', line 51

def normalize_coordinate(value, axis_tag)
  axis = find_axis(axis_tag)
  return 0.0 unless axis

  # Clamp to axis range
  value = [[value, axis.min_value].max, axis.max_value].min

  # Normalize to [-1, 1]
  if value < axis.default_value
    # Normalize between min and default (maps to -1..0)
    range = axis.default_value - axis.min_value
    return -1.0 if range.zero?

    (value - axis.default_value) / range
  elsif value > axis.default_value
    # Normalize between default and max (maps to 0..1)
    range = axis.max_value - axis.default_value
    return 1.0 if range.zero?

    (value - axis.default_value) / range
  else
    # At default value
    0.0
  end
end

#normalize_coordinates(coordinates) ⇒ Hash<String, Float>

Normalize all coordinates

Parameters:

  • coordinates (Hash<String, Float>)

    User-space coordinates

Returns:

  • (Hash<String, Float>)

    Normalized coordinates



81
82
83
84
85
86
87
88
89
# File 'lib/fontisan/variation/interpolator.rb', line 81

def normalize_coordinates(coordinates)
  result = {}
  @axes.each do |axis|
    tag = axis.axis_tag
    value = coordinates[tag] || axis.default_value
    result[tag] = normalize_coordinate(value, tag)
  end
  result
end