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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- 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
|