Class: Fontisan::Tables::CurveConverter
- Inherits:
-
Object
- Object
- Fontisan::Tables::CurveConverter
- 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.
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
-
.calculate_error(cubic, quadratics) ⇒ Float
Calculate maximum error between cubic and quadratic curves.
-
.cubic_to_quadratic(cubic, max_error: DEFAULT_MAX_ERROR) ⇒ Array<Hash>
Convert cubic Bézier to quadratic approximation.
-
.evaluate_cubic(cubic, t) ⇒ Hash
Evaluate cubic Bézier curve at parameter t.
-
.evaluate_quadratic(quad, t) ⇒ Hash
Evaluate quadratic Bézier curve at parameter t.
-
.quadratic_to_cubic(quad) ⇒ Hash
Convert quadratic Bézier to cubic (exact conversion).
-
.subdivide_cubic(cubic, t) ⇒ Array<Hash, Hash>
Subdivide cubic curve at parameter t using De Casteljau's algorithm.
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.
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.
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
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
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)
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
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 |