fontisan 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -4
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Converts between quadratic and cubic Bézier curves
6
+ #
7
+ # This class provides bidirectional conversion between TrueType's
8
+ # quadratic Bézier curves and CFF's cubic Bézier curves.
9
+ #
10
+ # **Quadratic → Cubic (Exact)**:
11
+ # Uses degree elevation formula to convert a quadratic Bézier curve
12
+ # into an equivalent cubic Bézier curve with 100% accuracy.
13
+ #
14
+ # **Cubic → Quadratic (Approximation)**:
15
+ # Uses adaptive subdivision to approximate a cubic Bézier curve with
16
+ # one or more quadratic curves, maintaining error within tolerance.
17
+ #
18
+ # @example Converting quadratic to cubic
19
+ # quad = { x0: 0, y0: 0, x1: 50, y1: 100, x2: 100, y2: 0 }
20
+ # cubic = CurveConverter.quadratic_to_cubic(quad)
21
+ # # => { x0: 0, y0: 0, x1: 33, y1: 67, x2: 67, y2: 67, x3: 100, y3: 0 }
22
+ #
23
+ # @example Converting cubic to quadratic
24
+ # cubic = { x0: 0, y0: 0, x1: 33, y1: 67, x2: 67, y2: 67, x3: 100, y3: 0 }
25
+ # quads = CurveConverter.cubic_to_quadratic(cubic, max_error: 0.5)
26
+ # # => [{ x0: 0, y0: 0, x1: 50, y1: 100, x2: 100, y2: 0 }]
27
+ class CurveConverter
28
+ # Default maximum error tolerance in font units
29
+ DEFAULT_MAX_ERROR = 0.5
30
+
31
+ # Number of samples for error measurement
32
+ ERROR_SAMPLE_COUNT = 11
33
+
34
+ # Convert quadratic Bézier to cubic (exact conversion)
35
+ #
36
+ # Uses degree elevation formula to convert a quadratic Bézier curve
37
+ # into an equivalent cubic Bézier curve. This conversion is exact
38
+ # with 100% accuracy.
39
+ #
40
+ # Formula:
41
+ # - CP0 = P0 (start point unchanged)
42
+ # - CP1 = P0 + 2/3 * (P1 - P0)
43
+ # - CP2 = P2 + 2/3 * (P1 - P2)
44
+ # - CP3 = P2 (end point unchanged)
45
+ #
46
+ # @param quad [Hash] Quadratic curve {:x0, :y0, :x1, :y1, :x2, :y2}
47
+ # @return [Hash] Cubic curve {:x0, :y0, :x1, :y1, :x2, :y2, :x3, :y3}
48
+ # @raise [ArgumentError] If quad is invalid
49
+ def self.quadratic_to_cubic(quad)
50
+ validate_quadratic_curve!(quad)
51
+
52
+ # P0 = start point
53
+ # P1 = control point
54
+ # P2 = end point
55
+ x0 = quad[:x0]
56
+ y0 = quad[:y0]
57
+ x1 = quad[:x1]
58
+ y1 = quad[:y1]
59
+ x2 = quad[:x2]
60
+ y2 = quad[:y2]
61
+
62
+ # Degree elevation formula
63
+ # CP1 = P0 + (2/3) * (P1 - P0)
64
+ cx1 = x0 + (2.0 / 3.0) * (x1 - x0)
65
+ cy1 = y0 + (2.0 / 3.0) * (y1 - y0)
66
+
67
+ # CP2 = P2 + (2/3) * (P1 - P2)
68
+ cx2 = x2 + (2.0 / 3.0) * (x1 - x2)
69
+ cy2 = y2 + (2.0 / 3.0) * (y1 - y2)
70
+
71
+ {
72
+ x0: x0,
73
+ y0: y0,
74
+ x1: cx1,
75
+ y1: cy1,
76
+ x2: cx2,
77
+ y2: cy2,
78
+ x3: x2,
79
+ y3: y2,
80
+ }
81
+ end
82
+
83
+ # Convert cubic Bézier to quadratic approximation
84
+ #
85
+ # Uses adaptive subdivision to approximate a cubic Bézier curve
86
+ # with one or more quadratic curves. The algorithm recursively
87
+ # subdivides the curve until the error is within tolerance.
88
+ #
89
+ # @param cubic [Hash] Cubic curve {:x0, :y0, :x1, :y1, :x2, :y2, :x3, :y3}
90
+ # @param max_error [Float] Maximum error tolerance (default: 0.5 units)
91
+ # @return [Array<Hash>] Array of quadratic curves
92
+ # @raise [ArgumentError] If parameters are invalid
93
+ def self.cubic_to_quadratic(cubic, max_error: DEFAULT_MAX_ERROR)
94
+ validate_cubic_curve!(cubic)
95
+ validate_max_error!(max_error)
96
+
97
+ # Try to approximate with a single quadratic curve
98
+ quad = approximate_cubic_with_quadratic(cubic)
99
+ error = calculate_error(cubic, [quad])
100
+
101
+ if error <= max_error
102
+ [quad]
103
+ else
104
+ # Subdivide and recursively approximate
105
+ left, right = subdivide_cubic(cubic, 0.5)
106
+ cubic_to_quadratic(left, max_error: max_error) +
107
+ cubic_to_quadratic(right, max_error: max_error)
108
+ end
109
+ end
110
+
111
+ # Calculate maximum error between cubic and quadratic curves
112
+ #
113
+ # Samples points along the curves and measures the maximum
114
+ # perpendicular distance between them.
115
+ #
116
+ # @param cubic [Hash] Original cubic curve
117
+ # @param quadratics [Array<Hash>] Approximating quadratic curves
118
+ # @return [Float] Maximum error distance
119
+ # @raise [ArgumentError] If parameters are invalid
120
+ def self.calculate_error(cubic, quadratics)
121
+ validate_cubic_curve!(cubic)
122
+ raise ArgumentError, "quadratics must be Array" unless quadratics.is_a?(Array)
123
+ raise ArgumentError, "quadratics cannot be empty" if quadratics.empty?
124
+
125
+ max_error = 0.0
126
+
127
+ # Sample points along the cubic curve
128
+ ERROR_SAMPLE_COUNT.times do |i|
129
+ t = i / (ERROR_SAMPLE_COUNT - 1.0)
130
+ cubic_point = evaluate_cubic(cubic, t)
131
+
132
+ # Find corresponding point on quadratic curves
133
+ quad_point = find_point_on_quadratics(quadratics, t)
134
+
135
+ # Calculate distance
136
+ dx = cubic_point[:x] - quad_point[:x]
137
+ dy = cubic_point[:y] - quad_point[:y]
138
+ distance = Math.sqrt(dx * dx + dy * dy)
139
+
140
+ max_error = distance if distance > max_error
141
+ end
142
+
143
+ max_error
144
+ end
145
+
146
+ # Subdivide cubic curve at parameter t using De Casteljau's algorithm
147
+ #
148
+ # @param cubic [Hash] Cubic curve to subdivide
149
+ # @param t [Float] Parameter value (0.0 to 1.0)
150
+ # @return [Array<Hash, Hash>] [left_curve, right_curve]
151
+ def self.subdivide_cubic(cubic, t)
152
+ validate_cubic_curve!(cubic)
153
+
154
+ x0 = cubic[:x0]
155
+ y0 = cubic[:y0]
156
+ x1 = cubic[:x1]
157
+ y1 = cubic[:y1]
158
+ x2 = cubic[:x2]
159
+ y2 = cubic[:y2]
160
+ x3 = cubic[:x3]
161
+ y3 = cubic[:y3]
162
+
163
+ # De Casteljau's algorithm
164
+ # First level
165
+ q0x = lerp(x0, x1, t)
166
+ q0y = lerp(y0, y1, t)
167
+ q1x = lerp(x1, x2, t)
168
+ q1y = lerp(y1, y2, t)
169
+ q2x = lerp(x2, x3, t)
170
+ q2y = lerp(y2, y3, t)
171
+
172
+ # Second level
173
+ r0x = lerp(q0x, q1x, t)
174
+ r0y = lerp(q0y, q1y, t)
175
+ r1x = lerp(q1x, q2x, t)
176
+ r1y = lerp(q1y, q2y, t)
177
+
178
+ # Third level (subdivision point)
179
+ sx = lerp(r0x, r1x, t)
180
+ sy = lerp(r0y, r1y, t)
181
+
182
+ left = {
183
+ x0: x0, y0: y0,
184
+ x1: q0x, y1: q0y,
185
+ x2: r0x, y2: r0y,
186
+ x3: sx, y3: sy
187
+ }
188
+
189
+ right = {
190
+ x0: sx, y0: sy,
191
+ x1: r1x, y1: r1y,
192
+ x2: q2x, y2: q2y,
193
+ x3: x3, y3: y3
194
+ }
195
+
196
+ [left, right]
197
+ end
198
+
199
+ # Evaluate cubic Bézier curve at parameter t
200
+ #
201
+ # @param cubic [Hash] Cubic curve
202
+ # @param t [Float] Parameter (0.0 to 1.0)
203
+ # @return [Hash] Point {:x, :y}
204
+ def self.evaluate_cubic(cubic, t)
205
+ x0 = cubic[:x0]
206
+ y0 = cubic[:y0]
207
+ x1 = cubic[:x1]
208
+ y1 = cubic[:y1]
209
+ x2 = cubic[:x2]
210
+ y2 = cubic[:y2]
211
+ x3 = cubic[:x3]
212
+ y3 = cubic[:y3]
213
+
214
+ # Cubic Bézier formula: B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
215
+ t2 = t * t
216
+ t3 = t2 * t
217
+ mt = 1.0 - t
218
+ mt2 = mt * mt
219
+ mt3 = mt2 * mt
220
+
221
+ x = mt3 * x0 + 3.0 * mt2 * t * x1 + 3.0 * mt * t2 * x2 + t3 * x3
222
+ y = mt3 * y0 + 3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3 * y3
223
+
224
+ { x: x, y: y }
225
+ end
226
+
227
+ # Evaluate quadratic Bézier curve at parameter t
228
+ #
229
+ # @param quad [Hash] Quadratic curve
230
+ # @param t [Float] Parameter (0.0 to 1.0)
231
+ # @return [Hash] Point {:x, :y}
232
+ def self.evaluate_quadratic(quad, t)
233
+ x0 = quad[:x0]
234
+ y0 = quad[:y0]
235
+ x1 = quad[:x1]
236
+ y1 = quad[:y1]
237
+ x2 = quad[:x2]
238
+ y2 = quad[:y2]
239
+
240
+ # Quadratic Bézier formula: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
241
+ t2 = t * t
242
+ mt = 1.0 - t
243
+ mt2 = mt * mt
244
+
245
+ x = mt2 * x0 + 2.0 * mt * t * x1 + t2 * x2
246
+ y = mt2 * y0 + 2.0 * mt * t * y1 + t2 * y2
247
+
248
+ { x: x, y: y }
249
+ end
250
+
251
+ private_class_method def self.approximate_cubic_with_quadratic(cubic)
252
+ validate_cubic_curve!(cubic)
253
+
254
+ x0 = cubic[:x0]
255
+ y0 = cubic[:y0]
256
+ x1 = cubic[:x1]
257
+ y1 = cubic[:y1]
258
+ x2 = cubic[:x2]
259
+ y2 = cubic[:y2]
260
+ x3 = cubic[:x3]
261
+ y3 = cubic[:y3]
262
+
263
+ # Better approximation: use weighted average that considers derivatives
264
+ # For optimal approximation, we want to match the curve shape
265
+ # Using the formula: C = 3/4*P1 + 3/4*P2 - 1/4*P0 - 1/4*P3
266
+ # This minimizes the maximum error for most curves
267
+ cx = 0.75 * x1 + 0.75 * x2 - 0.25 * x0 - 0.25 * x3
268
+ cy = 0.75 * y1 + 0.75 * y2 - 0.25 * y0 - 0.25 * y3
269
+
270
+ {
271
+ x0: x0,
272
+ y0: y0,
273
+ x1: cx,
274
+ y1: cy,
275
+ x2: x3,
276
+ y2: y3,
277
+ }
278
+ end
279
+
280
+ private_class_method def self.find_point_on_quadratics(quadratics, t)
281
+ # Determine which quadratic segment contains parameter t
282
+ segment_count = quadratics.length
283
+ segment_t = t * segment_count
284
+ segment_index = [segment_t.floor, segment_count - 1].min
285
+ local_t = segment_t - segment_index
286
+
287
+ evaluate_quadratic(quadratics[segment_index], local_t)
288
+ end
289
+
290
+ private_class_method def self.lerp(a, b, t)
291
+ a + t * (b - a)
292
+ end
293
+
294
+ private_class_method def self.validate_quadratic_curve!(quad)
295
+ unless quad.is_a?(Hash)
296
+ raise ArgumentError, "quad must be Hash, got: #{quad.class}"
297
+ end
298
+
299
+ required = %i[x0 y0 x1 y1 x2 y2]
300
+ missing = required - quad.keys
301
+ unless missing.empty?
302
+ raise ArgumentError, "quad missing keys: #{missing.join(', ')}"
303
+ end
304
+
305
+ required.each do |key|
306
+ value = quad[key]
307
+ unless value.is_a?(Numeric)
308
+ raise ArgumentError, "quad[:#{key}] must be Numeric, got: #{value.class}"
309
+ end
310
+ end
311
+ end
312
+
313
+ private_class_method def self.validate_cubic_curve!(cubic)
314
+ unless cubic.is_a?(Hash)
315
+ raise ArgumentError, "cubic must be Hash, got: #{cubic.class}"
316
+ end
317
+
318
+ required = %i[x0 y0 x1 y1 x2 y2 x3 y3]
319
+ missing = required - cubic.keys
320
+ unless missing.empty?
321
+ raise ArgumentError, "cubic missing keys: #{missing.join(', ')}"
322
+ end
323
+
324
+ required.each do |key|
325
+ value = cubic[key]
326
+ unless value.is_a?(Numeric)
327
+ raise ArgumentError, "cubic[:#{key}] must be Numeric, got: #{value.class}"
328
+ end
329
+ end
330
+ end
331
+
332
+ private_class_method def self.validate_max_error!(max_error)
333
+ unless max_error.is_a?(Numeric)
334
+ raise ArgumentError, "max_error must be Numeric, got: #{max_error.class}"
335
+ end
336
+
337
+ if max_error <= 0
338
+ raise ArgumentError, "max_error must be positive, got: #{max_error}"
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end