Class: Fontisan::Variation::RegionMatcher

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

Overview

Region matcher for variable fonts

This class matches design space coordinates to variation regions/tuples, determining which regions contribute to the final interpolated value and calculating their contribution scalars.

A variation region defines a sub-space within the design space where a particular set of deltas applies. Regions are defined by start, peak, and end coordinates on each axis.

Matching Process:

  1. For each region, check if current coordinates fall within the region

  2. Calculate the scalar (contribution factor) for each matching region

  3. Return only non-zero contributions

Reference: OpenType Font Variations specification, gvar table

Examples:

Matching coordinates to regions

matcher = RegionMatcher.new(axes)
matches = matcher.match_regions(
  coordinates: { "wght" => 600.0 },
  regions: [region1, region2, region3]
)
# => [{ region_index: 0, scalar: 0.5 }, { region_index: 1, scalar: 0.8 }]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(axes) ⇒ RegionMatcher

Initialize region matcher

Parameters:

  • axes (Array<VariationAxisRecord>)

    Variation axes from fvar table



39
40
41
42
# File 'lib/fontisan/variation/region_matcher.rb', line 39

def initialize(axes)
  @axes = axes || []
  @interpolator = Interpolator.new(@axes)
end

Instance Attribute Details

#axesArray<VariationAxisRecord> (readonly)

Returns Variation axes.

Returns:

  • (Array<VariationAxisRecord>)

    Variation axes



31
32
33
# File 'lib/fontisan/variation/region_matcher.rb', line 31

def axes
  @axes
end

#interpolatorInterpolator (readonly)

Returns Coordinate interpolator.

Returns:



34
35
36
# File 'lib/fontisan/variation/region_matcher.rb', line 34

def interpolator
  @interpolator
end

Instance Method Details

#active_region_indices(coordinates, regions) ⇒ Array<Integer>

Get active regions at coordinates

Returns the subset of regions that are active (non-zero contribution) at the given coordinates.

Parameters:

  • coordinates (Hash<String, Float>)

    User-space coordinates

  • regions (Array<Hash>)

    All regions

Returns:

  • (Array<Integer>)

    Indices of active regions



108
109
110
111
# File 'lib/fontisan/variation/region_matcher.rb', line 108

def active_region_indices(coordinates, regions)
  matches = match_regions(coordinates: coordinates, regions: regions)
  matches.map { |m| m[:region_index] }
end

#build_region_from_arrays(start_arr, peak_arr, end_arr) ⇒ Hash<String, Hash>

Build region from start, peak, end arrays

Converts array-based region data (as in gvar) to hash-based format.

Parameters:

  • start_arr (Array<Float>)

    Start coordinates (one per axis)

  • peak_arr (Array<Float>)

    Peak coordinates (one per axis)

  • end_arr (Array<Float>)

    End coordinates (one per axis)

Returns:

  • (Hash<String, Hash>)

    Region definition



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

def build_region_from_arrays(start_arr, peak_arr, end_arr)
  region = {}

  @axes.each_with_index do |axis, index|
    region[axis.axis_tag] = {
      start: start_arr[index] || -1.0,
      peak: peak_arr[index] || 0.0,
      end: end_arr[index] || 1.0,
    }
  end

  region
end

#build_region_from_peaks(peaks) ⇒ Hash<String, Hash>

Build region from peak coordinates

Creates a simple region definition from peak coordinates only, using ±1.0 for start/end on each axis.

Parameters:

  • peaks (Hash<String, Float>)

    Peak coordinates per axis

Returns:

  • (Hash<String, Hash>)

    Region definition



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/fontisan/variation/region_matcher.rb', line 159

def build_region_from_peaks(peaks)
  region = {}

  @axes.each do |axis|
    tag = axis.axis_tag
    peak = peaks[tag] || 0.0

    region[tag] = {
      start: peak.negative? ? -1.0 : 0.0,
      peak: peak,
      end: peak.positive? ? 1.0 : 0.0,
    }
  end

  region
end

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

Calculate contribution percentages for all regions

Returns the percentage contribution of each region at the given coordinates. All percentages sum to 100% (or less if some regions are inactive).

Parameters:

  • coordinates (Hash<String, Float>)

    User-space coordinates

  • regions (Array<Hash>)

    All regions

Returns:

  • (Array<Float>)

    Contribution percentages (0.0 to 1.0)



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/fontisan/variation/region_matcher.rb', line 122

def contribution_percentages(coordinates, regions)
  matches = match_regions(coordinates: coordinates, regions: regions)

  # Calculate total scalar
  total_scalar = matches.sum { |m| m[:scalar] }
  return Array.new(regions.size, 0.0) if total_scalar.zero?

  # Build percentage array
  percentages = Array.new(regions.size, 0.0)
  matches.each do |match|
    percentages[match[:region_index]] = match[:scalar] / total_scalar
  end

  percentages
end

#dominant_region(coordinates, regions) ⇒ Hash?

Find the dominant region at coordinates

Returns the region with the highest contribution scalar.

Parameters:

  • coordinates (Hash<String, Float>)

    User-space coordinates

  • regions (Array<Hash>)

    All regions

Returns:

  • (Hash, nil)

    Match with highest scalar or nil



145
146
147
148
149
150
# File 'lib/fontisan/variation/region_matcher.rb', line 145

def dominant_region(coordinates, regions)
  matches = match_regions(coordinates: coordinates, regions: regions)
  return nil if matches.empty?

  matches.max_by { |m| m[:scalar] }
end

#match_regions(coordinates:, regions:) ⇒ Array<Hash>

Match coordinates to variation regions

Returns all regions that contribute (have non-zero scalar) at the given coordinates, along with their contribution scalars.

Parameters:

  • coordinates (Hash<String, Float>)

    User-space coordinates

  • regions (Array<Hash>)

    Array of region definitions

Returns:

  • (Array<Hash>)

    Matches with :region_index and :scalar



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/fontisan/variation/region_matcher.rb', line 52

def match_regions(coordinates:, regions:)
  # Normalize coordinates
  normalized = @interpolator.normalize_coordinates(coordinates)

  # Find matching regions
  matches = []
  regions.each_with_index do |region, index|
    scalar = @interpolator.calculate_region_scalar(normalized, region)

    # Only include non-zero contributions
    matches << { region_index: index, scalar: scalar } if scalar > 0.0
  end

  matches
end

#match_tuples(coordinates:, tuples:) ⇒ Array<Hash>

Match coordinates to gvar tuple variations

Converts gvar tuple data to regions and matches them.

Parameters:

  • coordinates (Hash<String, Float>)

    User-space coordinates

  • tuples (Array<Hash>)

    Tuple variation data from gvar

Returns:

  • (Array<Hash>)

    Matches with :tuple_index and :scalar



75
76
77
78
79
80
81
82
83
# File 'lib/fontisan/variation/region_matcher.rb', line 75

def match_tuples(coordinates:, tuples:)
  # Convert tuples to regions
  regions = tuples.map do |tuple|
    @interpolator.build_region_from_tuple(tuple)
  end

  # Match regions
  match_regions(coordinates: coordinates, regions: regions)
end

#valid_region?(region) ⇒ Boolean

Validate region definition

Checks if a region is well-formed.

Parameters:

  • region (Hash<String, Hash>)

    Region definition

Returns:

  • (Boolean)

    True if valid



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

def valid_region?(region)
  return false unless region.is_a?(Hash)

  region.all? do |_axis_tag, axis_region|
    next false unless axis_region.is_a?(Hash)
    next false unless axis_region.key?(:peak)

    start_val = axis_region[:start] || -1.0
    peak = axis_region[:peak]
    end_val = axis_region[:end] || 1.0

    # Validate ordering: start <= peak <= end
    start_val <= peak && peak <= end_val
  end
end

#within_region?(coordinates, region) ⇒ Boolean

Check if coordinates are within a region

Parameters:

  • coordinates (Hash<String, Float>)

    Normalized coordinates

  • region (Hash<String, Hash>)

    Region definition per axis

Returns:

  • (Boolean)

    True if within region



90
91
92
93
94
95
96
97
98
# File 'lib/fontisan/variation/region_matcher.rb', line 90

def within_region?(coordinates, region)
  region.all? do |axis_tag, axis_region|
    coord = coordinates[axis_tag] || 0.0
    start_val = axis_region[:start] || -1.0
    end_val = axis_region[:end] || 1.0

    coord >= start_val && coord <= end_val
  end
end