Class: Fontisan::Tables::CurveConverter

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/tables/glyf/curve_converter.rb

Overview

Converts between quadratic and cubic Bézier curves

This class provides bidirectional conversion between TrueType’s quadratic Bézier curves and CFF’s cubic Bézier curves.

**Quadratic → Cubic (Exact)**: Uses degree elevation formula to convert a quadratic Bézier curve into an equivalent cubic Bézier curve with 100% accuracy.

**Cubic → Quadratic (Approximation)**: Uses adaptive subdivision to approximate a cubic Bézier curve with one or more quadratic curves, maintaining error within tolerance.

Examples:

Converting quadratic to cubic

quad = { x0: 0, y0: 0, x1: 50, y1: 100, x2: 100, y2: 0 }
cubic = CurveConverter.quadratic_to_cubic(quad)
# => { x0: 0, y0: 0, x1: 33, y1: 67, x2: 67, y2: 67, x3: 100, y3: 0 }

Converting cubic to quadratic

cubic = { x0: 0, y0: 0, x1: 33, y1: 67, x2: 67, y2: 67, x3: 100, y3: 0 }
quads = CurveConverter.cubic_to_quadratic(cubic, max_error: 0.5)
# => [{ x0: 0, y0: 0, x1: 50, y1: 100, x2: 100, y2: 0 }]

Constant Summary collapse

DEFAULT_MAX_ERROR =

Default maximum error tolerance in font units

0.5
ERROR_SAMPLE_COUNT =

Number of samples for error measurement

11

Class Method Summary collapse

Class Method Details

.calculate_error(cubic, quadratics) ⇒ Float

Calculate maximum error between cubic and quadratic curves

Samples points along the curves and measures the maximum perpendicular distance between them.

Parameters:

  • cubic (Hash)

    Original cubic curve

  • quadratics (Array<Hash>)

    Approximating quadratic curves

Returns:

  • (Float)

    Maximum error distance

Raises:

  • (ArgumentError)

    If parameters are invalid



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/fontisan/tables/glyf/curve_converter.rb', line 120

def self.calculate_error(cubic, quadratics)
  validate_cubic_curve!(cubic)
  unless quadratics.is_a?(Array)
    raise ArgumentError,
          "quadratics must be Array"
  end
  raise ArgumentError, "quadratics cannot be empty" if quadratics.empty?

  max_error = 0.0

  # Sample points along the cubic curve
  ERROR_SAMPLE_COUNT.times do |i|
    t = i / (ERROR_SAMPLE_COUNT - 1.0)
    cubic_point = evaluate_cubic(cubic, t)

    # Find corresponding point on quadratic curves
    quad_point = find_point_on_quadratics(quadratics, t)

    # Calculate distance
    dx = cubic_point[:x] - quad_point[:x]
    dy = cubic_point[:y] - quad_point[:y]
    distance = Math.sqrt(dx * dx + dy * dy)

    max_error = distance if distance > max_error
  end

  max_error
end

.cubic_to_quadratic(cubic, max_error: DEFAULT_MAX_ERROR) ⇒ Array<Hash>

Convert cubic Bézier to quadratic approximation

Uses adaptive subdivision to approximate a cubic Bézier curve with one or more quadratic curves. The algorithm recursively subdivides the curve until the error is within tolerance.

Parameters:

  • cubic (Hash)

    Cubic curve :y0, :x1, :y1, :x2, :y2, :x3, :y3

  • max_error (Float) (defaults to: DEFAULT_MAX_ERROR)

    Maximum error tolerance (default: 0.5 units)

Returns:

  • (Array<Hash>)

    Array of quadratic curves

Raises:

  • (ArgumentError)

    If parameters are invalid



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/fontisan/tables/glyf/curve_converter.rb', line 93

def self.cubic_to_quadratic(cubic, max_error: DEFAULT_MAX_ERROR)
  validate_cubic_curve!(cubic)
  validate_max_error!(max_error)

  # Try to approximate with a single quadratic curve
  quad = approximate_cubic_with_quadratic(cubic)
  error = calculate_error(cubic, [quad])

  if error <= max_error
    [quad]
  else
    # Subdivide and recursively approximate
    left, right = subdivide_cubic(cubic, 0.5)
    cubic_to_quadratic(left, max_error: max_error) +
      cubic_to_quadratic(right, max_error: max_error)
  end
end

.evaluate_cubic(cubic, t) ⇒ Hash

Evaluate cubic Bézier curve at parameter t

Parameters:

  • cubic (Hash)

    Cubic curve

  • t (Float)

    Parameter (0.0 to 1.0)

Returns:

  • (Hash)

    Point :y



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/fontisan/tables/glyf/curve_converter.rb', line 207

def self.evaluate_cubic(cubic, t)
  x0 = cubic[:x0]
  y0 = cubic[:y0]
  x1 = cubic[:x1]
  y1 = cubic[:y1]
  x2 = cubic[:x2]
  y2 = cubic[:y2]
  x3 = cubic[:x3]
  y3 = cubic[:y3]

  # Cubic Bézier formula: B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
  t2 = t * t
  t3 = t2 * t
  mt = 1.0 - t
  mt2 = mt * mt
  mt3 = mt2 * mt

  x = mt3 * x0 + 3.0 * mt2 * t * x1 + 3.0 * mt * t2 * x2 + t3 * x3
  y = mt3 * y0 + 3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3 * y3

  { x: x, y: y }
end

.evaluate_quadratic(quad, t) ⇒ Hash

Evaluate quadratic Bézier curve at parameter t

Parameters:

  • quad (Hash)

    Quadratic curve

  • t (Float)

    Parameter (0.0 to 1.0)

Returns:

  • (Hash)

    Point :y



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/fontisan/tables/glyf/curve_converter.rb', line 235

def self.evaluate_quadratic(quad, t)
  x0 = quad[:x0]
  y0 = quad[:y0]
  x1 = quad[:x1]
  y1 = quad[:y1]
  x2 = quad[:x2]
  y2 = quad[:y2]

  # Quadratic Bézier formula: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
  t2 = t * t
  mt = 1.0 - t
  mt2 = mt * mt

  x = mt2 * x0 + 2.0 * mt * t * x1 + t2 * x2
  y = mt2 * y0 + 2.0 * mt * t * y1 + t2 * y2

  { x: x, y: y }
end

.quadratic_to_cubic(quad) ⇒ Hash

Convert quadratic Bézier to cubic (exact conversion)

Uses degree elevation formula to convert a quadratic Bézier curve into an equivalent cubic Bézier curve. This conversion is exact with 100% accuracy.

Formula:

  • CP0 = P0 (start point unchanged)

  • CP1 = P0 + 2/3 * (P1 - P0)

  • CP2 = P2 + 2/3 * (P1 - P2)

  • CP3 = P2 (end point unchanged)

Parameters:

  • quad (Hash)

    Quadratic curve :y0, :x1, :y1, :x2, :y2

Returns:

  • (Hash)

    Cubic curve :y0, :x1, :y1, :x2, :y2, :x3, :y3

Raises:

  • (ArgumentError)

    If quad is invalid



49
50
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
76
77
78
79
80
81
# File 'lib/fontisan/tables/glyf/curve_converter.rb', line 49

def self.quadratic_to_cubic(quad)
  validate_quadratic_curve!(quad)

  # P0 = start point
  # P1 = control point
  # P2 = end point
  x0 = quad[:x0]
  y0 = quad[:y0]
  x1 = quad[:x1]
  y1 = quad[:y1]
  x2 = quad[:x2]
  y2 = quad[:y2]

  # Degree elevation formula
  # CP1 = P0 + (2/3) * (P1 - P0)
  cx1 = x0 + (2.0 / 3.0) * (x1 - x0)
  cy1 = y0 + (2.0 / 3.0) * (y1 - y0)

  # CP2 = P2 + (2/3) * (P1 - P2)
  cx2 = x2 + (2.0 / 3.0) * (x1 - x2)
  cy2 = y2 + (2.0 / 3.0) * (y1 - y2)

  {
    x0: x0,
    y0: y0,
    x1: cx1,
    y1: cy1,
    x2: cx2,
    y2: cy2,
    x3: x2,
    y3: y2,
  }
end

.subdivide_cubic(cubic, t) ⇒ Array<Hash, Hash>

Subdivide cubic curve at parameter t using De Casteljau’s algorithm

Parameters:

  • cubic (Hash)

    Cubic curve to subdivide

  • t (Float)

    Parameter value (0.0 to 1.0)

Returns:

  • (Array<Hash, Hash>)
    left_curve, right_curve


154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/fontisan/tables/glyf/curve_converter.rb', line 154

def self.subdivide_cubic(cubic, t)
  validate_cubic_curve!(cubic)

  x0 = cubic[:x0]
  y0 = cubic[:y0]
  x1 = cubic[:x1]
  y1 = cubic[:y1]
  x2 = cubic[:x2]
  y2 = cubic[:y2]
  x3 = cubic[:x3]
  y3 = cubic[:y3]

  # De Casteljau's algorithm
  # First level
  q0x = lerp(x0, x1, t)
  q0y = lerp(y0, y1, t)
  q1x = lerp(x1, x2, t)
  q1y = lerp(y1, y2, t)
  q2x = lerp(x2, x3, t)
  q2y = lerp(y2, y3, t)

  # Second level
  r0x = lerp(q0x, q1x, t)
  r0y = lerp(q0y, q1y, t)
  r1x = lerp(q1x, q2x, t)
  r1y = lerp(q1y, q2y, t)

  # Third level (subdivision point)
  sx = lerp(r0x, r1x, t)
  sy = lerp(r0y, r1y, t)

  left = {
    x0: x0, y0: y0,
    x1: q0x, y1: q0y,
    x2: r0x, y2: r0y,
    x3: sx, y3: sy
  }

  right = {
    x0: sx, y0: sy,
    x1: r1x, y1: r1y,
    x2: q2x, y2: q2y,
    x3: x3, y3: y3
  }

  [left, right]
end