mtg_card_maker 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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE.txt +59 -14
  4. data/README.md +251 -69
  5. data/lib/mtg_card_maker/base_card.rb +24 -1
  6. data/lib/mtg_card_maker/cli.rb +44 -41
  7. data/lib/mtg_card_maker/color_scheme.rb +18 -23
  8. data/lib/mtg_card_maker/css_service.rb +72 -0
  9. data/lib/mtg_card_maker/fonts/GoudyBookletter1911.ttf +0 -0
  10. data/lib/mtg_card_maker/fonts/OFL.txt +92 -0
  11. data/lib/mtg_card_maker/fonts/goudy_bookletter_base64.txt +1 -0
  12. data/lib/mtg_card_maker/icon_filename_service.rb +118 -0
  13. data/lib/mtg_card_maker/icon_service.rb +87 -25
  14. data/lib/mtg_card_maker/icons/black.svg +6 -1
  15. data/lib/mtg_card_maker/icons/blue.svg +6 -1
  16. data/lib/mtg_card_maker/icons/colorless.svg +6 -1
  17. data/lib/mtg_card_maker/icons/double-digit.svg +6 -0
  18. data/lib/mtg_card_maker/icons/energy.svg +26 -0
  19. data/lib/mtg_card_maker/icons/green.svg +6 -1
  20. data/lib/mtg_card_maker/icons/hybrid/2-black.svg +19 -0
  21. data/lib/mtg_card_maker/icons/hybrid/2-blue.svg +19 -0
  22. data/lib/mtg_card_maker/icons/hybrid/2-green.svg +19 -0
  23. data/lib/mtg_card_maker/icons/hybrid/2-red.svg +19 -0
  24. data/lib/mtg_card_maker/icons/hybrid/2-white.svg +19 -0
  25. data/lib/mtg_card_maker/icons/hybrid/black-blue.svg +19 -0
  26. data/lib/mtg_card_maker/icons/hybrid/black-green.svg +19 -0
  27. data/lib/mtg_card_maker/icons/hybrid/black-red.svg +19 -0
  28. data/lib/mtg_card_maker/icons/hybrid/black-white.svg +19 -0
  29. data/lib/mtg_card_maker/icons/hybrid/blue-black.svg +19 -0
  30. data/lib/mtg_card_maker/icons/hybrid/blue-green.svg +19 -0
  31. data/lib/mtg_card_maker/icons/hybrid/blue-red.svg +19 -0
  32. data/lib/mtg_card_maker/icons/hybrid/blue-white.svg +19 -0
  33. data/lib/mtg_card_maker/icons/hybrid/colorless-black.svg +19 -0
  34. data/lib/mtg_card_maker/icons/hybrid/colorless-blue.svg +19 -0
  35. data/lib/mtg_card_maker/icons/hybrid/colorless-green.svg +19 -0
  36. data/lib/mtg_card_maker/icons/hybrid/colorless-red.svg +19 -0
  37. data/lib/mtg_card_maker/icons/hybrid/colorless-white.svg +19 -0
  38. data/lib/mtg_card_maker/icons/hybrid/green-black.svg +19 -0
  39. data/lib/mtg_card_maker/icons/hybrid/green-blue.svg +19 -0
  40. data/lib/mtg_card_maker/icons/hybrid/green-red.svg +19 -0
  41. data/lib/mtg_card_maker/icons/hybrid/green-white.svg +19 -0
  42. data/lib/mtg_card_maker/icons/hybrid/red-black.svg +19 -0
  43. data/lib/mtg_card_maker/icons/hybrid/red-blue.svg +19 -0
  44. data/lib/mtg_card_maker/icons/hybrid/red-green.svg +19 -0
  45. data/lib/mtg_card_maker/icons/hybrid/red-white.svg +19 -0
  46. data/lib/mtg_card_maker/icons/hybrid/white-black.svg +19 -0
  47. data/lib/mtg_card_maker/icons/hybrid/white-blue.svg +19 -0
  48. data/lib/mtg_card_maker/icons/hybrid/white-green.svg +19 -0
  49. data/lib/mtg_card_maker/icons/hybrid/white-red.svg +19 -0
  50. data/lib/mtg_card_maker/icons/phyrexian/black-blue.svg +19 -0
  51. data/lib/mtg_card_maker/icons/phyrexian/black-green.svg +19 -0
  52. data/lib/mtg_card_maker/icons/phyrexian/black-red.svg +19 -0
  53. data/lib/mtg_card_maker/icons/phyrexian/black-white.svg +19 -0
  54. data/lib/mtg_card_maker/icons/phyrexian/black.svg +6 -0
  55. data/lib/mtg_card_maker/icons/phyrexian/blue-black.svg +19 -0
  56. data/lib/mtg_card_maker/icons/phyrexian/blue-green.svg +19 -0
  57. data/lib/mtg_card_maker/icons/phyrexian/blue-red.svg +19 -0
  58. data/lib/mtg_card_maker/icons/phyrexian/blue-white.svg +19 -0
  59. data/lib/mtg_card_maker/icons/phyrexian/blue.svg +6 -0
  60. data/lib/mtg_card_maker/icons/phyrexian/colorless.svg +6 -0
  61. data/lib/mtg_card_maker/icons/phyrexian/green-black.svg +19 -0
  62. data/lib/mtg_card_maker/icons/phyrexian/green-blue.svg +19 -0
  63. data/lib/mtg_card_maker/icons/phyrexian/green-red.svg +19 -0
  64. data/lib/mtg_card_maker/icons/phyrexian/green-white.svg +19 -0
  65. data/lib/mtg_card_maker/icons/phyrexian/green.svg +6 -0
  66. data/lib/mtg_card_maker/icons/phyrexian/red-black.svg +19 -0
  67. data/lib/mtg_card_maker/icons/phyrexian/red-blue.svg +19 -0
  68. data/lib/mtg_card_maker/icons/phyrexian/red-green.svg +19 -0
  69. data/lib/mtg_card_maker/icons/phyrexian/red-white.svg +19 -0
  70. data/lib/mtg_card_maker/icons/phyrexian/red.svg +6 -0
  71. data/lib/mtg_card_maker/icons/phyrexian/white-black.svg +19 -0
  72. data/lib/mtg_card_maker/icons/phyrexian/white-blue.svg +19 -0
  73. data/lib/mtg_card_maker/icons/phyrexian/white-green.svg +19 -0
  74. data/lib/mtg_card_maker/icons/phyrexian/white-red.svg +19 -0
  75. data/lib/mtg_card_maker/icons/phyrexian/white.svg +6 -0
  76. data/lib/mtg_card_maker/icons/red.svg +6 -1
  77. data/lib/mtg_card_maker/icons/single-digit.svg +6 -0
  78. data/lib/mtg_card_maker/icons/snow.svg +24 -0
  79. data/lib/mtg_card_maker/icons/tap.svg +1 -0
  80. data/lib/mtg_card_maker/icons/tap_legacy.svg +45 -0
  81. data/lib/mtg_card_maker/icons/untap.svg +1 -0
  82. data/lib/mtg_card_maker/icons/white.svg +6 -1
  83. data/lib/mtg_card_maker/icons/x.svg +6 -0
  84. data/lib/mtg_card_maker/layer_config.rb +21 -46
  85. data/lib/mtg_card_maker/layers/border_layer.rb +4 -5
  86. data/lib/mtg_card_maker/layers/frame_layer.rb +1 -1
  87. data/lib/mtg_card_maker/layers/name_layer.rb +16 -8
  88. data/lib/mtg_card_maker/layers/power_layer.rb +1 -3
  89. data/lib/mtg_card_maker/layers/text_box_layer.rb +205 -51
  90. data/lib/mtg_card_maker/layers/type_line_layer.rb +2 -4
  91. data/lib/mtg_card_maker/logger.rb +62 -0
  92. data/lib/mtg_card_maker/mana_cost.rb +59 -128
  93. data/lib/mtg_card_maker/sprite_sheet_assets.rb +10 -55
  94. data/lib/mtg_card_maker/sprite_sheet_builder.rb +20 -8
  95. data/lib/mtg_card_maker/sprite_sheet_service.rb +22 -9
  96. data/lib/mtg_card_maker/svg_gradient_service.rb +11 -11
  97. data/lib/mtg_card_maker/symbol_replacement_service.rb +102 -0
  98. data/lib/mtg_card_maker/text_rendering_service.rb +7 -3
  99. data/lib/mtg_card_maker/version.rb +1 -1
  100. data/lib/mtg_card_maker.rb +9 -78
  101. metadata +79 -12
  102. data/lib/mtg_card_maker/fonts/Goudy Mediaeval DemiBold.ttf +0 -0
  103. data/lib/mtg_card_maker/fonts/goudy_base64.txt +0 -1
@@ -5,6 +5,7 @@ module MtgCardMaker
5
5
  # This is the small area in the center of the card below the artwork
6
6
  class TypeLineLayer < BaseLayer
7
7
  include LayerInitializer
8
+
8
9
  attr_reader :type_line, :color_scheme
9
10
 
10
11
  def initialize(dimensions:, type_line:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
@@ -15,9 +16,6 @@ module MtgCardMaker
15
16
  end
16
17
 
17
18
  def render
18
- # Ensure gradients are defined for this color scheme
19
- SvgGradientService.define_all_gradients(svg, color_scheme)
20
-
21
19
  svg.g do
22
20
  render_type_background
23
21
  render_type_line
@@ -76,7 +74,7 @@ module MtgCardMaker
76
74
  y: layer_config.text_y_position(y, :type_area, height),
77
75
  font_size: layer_config.font_size(:type),
78
76
  available_width: layer_config.text_width(width, :type_area),
79
- css_class: layer_config.css_class(:card_type)
77
+ css_class: layer_config.css_class(:type_line)
80
78
  )
81
79
  text_service.wrapped_text_lines.each do |line, attrs|
82
80
  svg.text line, attrs
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module MtgCardMaker
6
+ # A logger wrapper around Ruby's standard Logger that adds emoji to output
7
+ # and provides separate streams for info (stdout) and warnings/errors (stderr).
8
+ #
9
+ # @example Basic usage
10
+ # logger = MtgCardMaker::Logger.new
11
+ # logger.info("Card generated successfully!")
12
+ # logger.warn("Warning: temp file cleanup failed")
13
+ # logger.error("Error: invalid configuration")
14
+ #
15
+ # @since 0.1.0
16
+ class Logger
17
+ # Initialize a new logger instance
18
+ #
19
+ # @param output_stream [IO] the output stream for info messages (default: $stdout)
20
+ # @param error_stream [IO] the error stream for warnings/errors (default: $stderr)
21
+ def initialize(output_stream: $stdout, error_stream: $stderr)
22
+ @info_logger = ::Logger.new(output_stream)
23
+ @error_logger = ::Logger.new(error_stream)
24
+
25
+ # Set simple formatters that just output the message
26
+ @info_logger.formatter = proc { |_severity, _datetime, _progname, msg| "#{msg}\n" }
27
+ @error_logger.formatter = proc { |_severity, _datetime, _progname, msg| "#{msg}\n" }
28
+ end
29
+
30
+ # Log an info message to stdout
31
+ #
32
+ # @param message [String] the message to log
33
+ # @return [void]
34
+ def info(message)
35
+ @info_logger.info(message)
36
+ end
37
+
38
+ # Log a warning message to stderr
39
+ #
40
+ # @param message [String] the warning message to log
41
+ # @return [void]
42
+ def warn(message)
43
+ @error_logger.warn("❌ #{message}")
44
+ end
45
+
46
+ # Log an error message to stderr
47
+ #
48
+ # @param message [String] the error message to log
49
+ # @return [void]
50
+ def error(message)
51
+ @error_logger.error("💥 Error: #{message}")
52
+ end
53
+
54
+ # Log a success message with sparkle emoji to stdout
55
+ #
56
+ # @param message [String] the success message to log
57
+ # @return [void]
58
+ def success(message)
59
+ info("✨ #{message} ✨")
60
+ end
61
+ end
62
+ end
@@ -5,7 +5,7 @@ require_relative 'icon_service'
5
5
  module MtgCardMaker
6
6
  # Mana cost class that generates SVG for the mana cost of a card.
7
7
  # This class parses mana cost strings (e.g., "2UR", "XG") and generates
8
- # SVG circles with appropriate colors and icons for each mana symbol.
8
+ # SVG icons for each mana symbol.
9
9
  #
10
10
  # @example
11
11
  # mana_cost = MtgCardMaker::ManaCost.new("2UR")
@@ -16,69 +16,46 @@ module MtgCardMaker
16
16
  # svg = mana_cost.to_svg
17
17
  #
18
18
  # @since 0.1.0
19
- # rubocop:disable Metrics/ClassLength
20
19
  class ManaCost
21
- # Mapping of letters to colors
22
- # @return [Hash] the color mapping for mana symbols
23
- COLOR_MAP = {
24
- 'B' => :black, 'S' => :black,
25
- 'U' => :blue, 'I' => :blue,
26
- 'G' => :green, 'F' => :green,
27
- 'W' => :white, 'P' => :white,
28
- 'R' => :red, 'M' => :red,
29
- 'C' => :colorless
30
- }.freeze
31
-
32
- # @return [Array<Symbol>] the parsed mana elements
20
+ # @return [Array<String>] the parsed mana elements (symbol strings)
33
21
  attr_reader :elements
34
22
 
35
23
  # @return [Integer, nil] the integer value for generic mana
36
24
  attr_reader :int_val
37
25
 
26
+ # @return [LayerConfig] the layer configuration
27
+ attr_reader :layer_config
28
+
38
29
  # Initialize a new mana cost parser
39
30
  #
40
31
  # @param mana_string [String, nil] the mana cost string (e.g., "2UR", "XG")
41
32
  # @param icon_set [Symbol] the icon set to use (default: :default)
42
- def initialize(mana_string = nil, icon_set = :default) # rubocop:disable Metrics/MethodLength
33
+ def initialize(mana_string = nil, icon_set = :default)
43
34
  @elements = []
44
- @origins = [] # :numeric, :C, or color symbol
45
35
  @int_val = nil
46
- @original_string = mana_string
47
36
  @icon_service = IconService.new(icon_set)
37
+ @layer_config = LayerConfig.default
48
38
 
49
39
  return if mana_string.nil? || mana_string.empty?
50
40
 
51
- # Convert to uppercase for consistency
52
- mana_string = mana_string.to_s.upcase
53
-
54
- # Parse the mana string
55
- parse_mana_string(mana_string)
56
-
57
- # Limit to maximum circles
58
- layer_config = LayerConfig.default
59
- max_circles = layer_config.mana_cost_config[:max_circles]
60
- @elements = @elements.first(max_circles)
61
- @origins = @origins.first(max_circles)
41
+ parse_mana_string(mana_string.to_s.upcase)
42
+ limit_elements
62
43
  end
63
44
 
64
- # Returns SVG string for the mana cost circles with drop shadow
45
+ # Returns SVG string for the mana cost icons with drop shadow
65
46
  #
66
47
  # @return [String] the SVG markup for the mana cost
67
48
  def to_svg
68
- layer_config = LayerConfig.default
69
49
  circle_spacing = layer_config.mana_cost_config[:circle_spacing]
70
50
 
71
- # Build mana cost circles SVG
72
- mana_circles = @elements.each_with_index.map do |color, i|
73
- x = i * circle_spacing
74
- y = 0
75
- mana_element_svg(x, y, color, @origins[i])
51
+ mana_icons = @elements.each_with_index.map do |symbol, i|
52
+ render_mana_icon(i * circle_spacing, 0, symbol)
76
53
  end.join
77
54
 
78
55
  <<~SVG.delete("\n")
79
56
  #{drop_shadow_filter}
80
57
  <g filter="url(#mana-cost-drop-shadow)">
81
- #{mana_circles}
58
+ #{mana_icons}
82
59
  </g>
83
60
  SVG
84
61
  end
@@ -86,61 +63,74 @@ module MtgCardMaker
86
63
  private
87
64
 
88
65
  def parse_mana_string(mana_string)
89
- return if mana_string.nil?
90
-
91
- if numeric_cost?(mana_string) || mana_string.start_with?('X')
66
+ if mana_string.start_with?('X')
67
+ parse_x_cost(mana_string)
68
+ elsif mana_string.match?(/^\d+/)
92
69
  parse_numeric_cost(mana_string)
93
70
  else
94
71
  parse_colored_mana(mana_string)
95
72
  end
96
73
  end
97
74
 
98
- def numeric_cost?(mana_string)
99
- mana_string.match?(/^\d+/)
75
+ def parse_x_cost(mana_string)
76
+ @elements << '{X}'
77
+ remaining_string = mana_string[1..]
78
+
79
+ # Check if the remaining string starts with a number
80
+ if remaining_string.match?(/^\d+/)
81
+ parse_numeric_cost(remaining_string)
82
+ else
83
+ parse_colored_mana(remaining_string)
84
+ end
100
85
  end
101
86
 
102
87
  def parse_numeric_cost(mana_string)
103
- mana_string = mana_string.gsub(/^X/, '10')
104
-
105
- int_match = mana_string.match(/^(\d+)/)
106
- @int_val = int_match[1].to_i
88
+ numeric_value = mana_string.match(/^(\d+)/)[1].to_i
107
89
 
108
- # If it's a double-digit number, treat as X
109
- @int_val = 10 if @int_val >= 10
110
-
111
- @elements << :colorless
112
- @origins << :numeric
90
+ if numeric_value >= 100
91
+ @int_val = nil
92
+ @elements << '{X}'
93
+ else
94
+ @int_val = numeric_value
95
+ @elements << "{#{numeric_value}}"
96
+ end
113
97
 
114
- # Remove the integer and parse remaining colored mana
115
- remaining = mana_string[int_match[1].length..]
116
- parse_colored_mana(remaining)
98
+ parse_colored_mana(mana_string[numeric_value.to_s.length..])
117
99
  end
118
100
 
119
101
  def parse_colored_mana(mana_string)
120
- return if mana_string.nil? || mana_string.empty?
102
+ # Use regex to match symbols: single letters, 1-2 digits, or {anything between curly braces}
103
+ mana_string.scan(/([A-Z]|\d{1,2}|\{[^}]+\})/).flatten.each do |symbol|
104
+ # Add curly braces if not already present
105
+ formatted_symbol = symbol.start_with?('{') ? symbol : "{#{symbol}}"
121
106
 
122
- mana_string.chars.each do |char|
123
- process_colored_character(char)
107
+ # Allow any symbol - IconService will handle validation
108
+ @elements << formatted_symbol
124
109
  end
125
110
  end
126
111
 
127
- def process_colored_character(char)
128
- color = COLOR_MAP[char]
129
- if colorless_symbol?(color, char)
130
- @elements << :colorless
131
- @origins << :C
132
- elsif color
133
- @elements << color
134
- @origins << color
135
- end
112
+ def limit_elements
113
+ max_circles = layer_config.mana_cost_config[:max_circles]
114
+ @elements = @elements.first(max_circles)
136
115
  end
137
116
 
138
- def colorless_symbol?(color, char)
139
- color == :colorless && char == 'C'
117
+ def render_mana_icon(x, y, symbol)
118
+ icon_size = if symbol.match?(%r{\{.*/.*\}}) # Hybrid or phyrexian symbols
119
+ layer_config.mana_cost_config[:hybrid_icon_size]
120
+ else
121
+ layer_config.mana_cost_config[:icon_size]
122
+ end
123
+
124
+ icon_svg = @icon_service.symbol_svg(symbol, size: icon_size)
125
+ return unless icon_svg
126
+
127
+ icon_x = x - (icon_size / 2)
128
+ icon_y = y - (icon_size / 2)
129
+
130
+ "<g transform='translate(#{icon_x}, #{icon_y})'>#{icon_svg}</g>"
140
131
  end
141
132
 
142
133
  def drop_shadow_filter
143
- layer_config = LayerConfig.default
144
134
  drop_shadow = layer_config.drop_shadow_config
145
135
  <<~SVG.delete("\n")
146
136
  <defs>
@@ -152,69 +142,10 @@ module MtgCardMaker
152
142
  <feDropShadow dx="#{drop_shadow[:dx]}"
153
143
  dy="#{drop_shadow[:dy]}"
154
144
  stdDeviation="#{drop_shadow[:std_deviation]}"
155
- flood-color="black"
156
- flood-opacity="#{drop_shadow[:flood_opacity]}"/>
145
+ flood-color="black"/>
157
146
  </filter>
158
147
  </defs>
159
148
  SVG
160
149
  end
161
-
162
- def mana_element_svg(x, y, color, origin) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
163
- layer_config = LayerConfig.default
164
- circle_radius = layer_config.mana_cost_config[:circle_radius]
165
- icon_size = layer_config.mana_cost_config[:icon_size]
166
-
167
- fill = svg_color(color)
168
- svg = "<circle cx='#{x}' cy='#{y}' r='#{circle_radius}' fill='#{fill}' />"
169
-
170
- if color == :colorless && origin == :numeric
171
- # Add text for numeric colorless mana circles
172
- svg << colorless_text(x, y, origin)
173
- else
174
- # Add icon inside colored mana circles, smaller and with opacity
175
- icon_svg = @icon_service.icon_svg(color, size: icon_size)
176
- if icon_svg
177
- icon_x = x - (icon_size / 2)
178
- icon_y = y - (icon_size / 2)
179
- layer_config = LayerConfig.default
180
- opacity = layer_config.mana_cost_icon_opacity
181
- svg << "<g transform='translate(#{icon_x}, #{icon_y})' opacity='#{opacity}'>#{icon_svg}</g>"
182
- end
183
- end
184
-
185
- svg
186
- end
187
-
188
- def colorless_text(x, y, origin)
189
- if origin == :numeric && @int_val
190
- text = @int_val == 10 ? 'X' : @int_val.to_s
191
- text_svg(x, y, text)
192
- else
193
- ''
194
- end
195
- end
196
-
197
- def text_svg(x, y, text)
198
- # Position text in the center of the circle with appropriate styling
199
- layer_config = LayerConfig.default
200
- font_size = layer_config.mana_cost_font_size
201
- font_weight = text == 'X' ? 'normal' : 'semibold'
202
- y_offset = layer_config.mana_cost_text_y_offset
203
-
204
- "<text x='#{x}' y='#{y + y_offset}' fill='#000' text-anchor='middle' " \
205
- "font-weight='#{font_weight}' font-size='#{font_size}' font-family='serif'>#{text}</text>"
206
- end
207
-
208
- def svg_color(color)
209
- case color
210
- when :white then '#FFF9C4' # White primary color
211
- when :blue then '#90CAF9' # Blue primary color (from fixture)
212
- when :black then '#BDBDBD' # Black primary color (from fixture)
213
- when :red then '#EF9A9A' # Red primary color (from fixture)
214
- when :green then '#A5D6A7' # Green primary color (from fixture)
215
- else '#DDD'
216
- end
217
- end
218
150
  end
219
- # rubocop:enable Metrics/ClassLength
220
151
  end
@@ -8,67 +8,21 @@ module MtgCardMaker
8
8
  end
9
9
 
10
10
  # Add all shared assets to the sprite sheet XML
11
- def add_assets_to_sprite(xml)
12
- add_sprite_styles(xml)
11
+ #
12
+ # @param xml [Nokogiri::XML::Builder] the XML builder for the sprite sheet
13
+ # @param embed_font [Boolean] whether to embed the font as base64 (default: true)
14
+ def add_assets_to_sprite(xml, embed_font: true)
15
+ add_sprite_styles(xml, embed_font: embed_font)
13
16
  add_sprite_masks(xml)
14
17
  add_sprite_gradients(xml)
15
18
  end
16
19
 
17
20
  private
18
21
 
19
- def add_sprite_styles(xml)
22
+ def add_sprite_styles(xml, embed_font)
20
23
  xml.defs do
21
24
  xml.style(type: 'text/css') do
22
- xml.cdata <<~CSS
23
- @font-face {
24
- font-family: 'Goudy Mediaeval DemiBold';
25
- src: url('fonts/Goudy Mediaeval DemiBold.ttf') format('truetype');
26
- font-weight: normal;
27
- font-style: normal;
28
- }
29
-
30
- .card-name {
31
- font-family: 'Goudy Mediaeval DemiBold', serif;
32
- font-weight: normal;
33
- font-style: normal;
34
- }
35
-
36
- .card-type {
37
- font-family: 'Goudy Mediaeval DemiBold', serif;
38
- font-weight: normal;
39
- font-style: normal;
40
- }
41
-
42
- .card-description {
43
- font-family: serif;
44
- font-weight: normal;
45
- font-style: normal;
46
- }
47
-
48
- .card-flavor-text {
49
- font-family: serif;
50
- font-weight: normal;
51
- font-style: italic;
52
- }
53
-
54
- .card-power-toughness {
55
- font-family: serif;
56
- font-weight: bold;
57
- font-style: normal;
58
- }
59
-
60
- .mana-cost-text {
61
- font-family: serif;
62
- font-weight: normal;
63
- font-style: normal;
64
- }
65
-
66
- .mana-cost-text-large {
67
- font-family: serif;
68
- font-weight: semibold;
69
- font-style: normal;
70
- }
71
- CSS
25
+ xml.cdata CssService.complete_styles(embed: embed_font)
72
26
  end
73
27
  end
74
28
  end
@@ -108,8 +62,8 @@ module MtgCardMaker
108
62
  SvgGradientService.define_frame_gradient(xml, color_scheme, scheme_name)
109
63
  when 'name'
110
64
  SvgGradientService.define_name_gradient(xml, color_scheme, scheme_name)
111
- when 'description'
112
- SvgGradientService.define_description_gradient(xml, color_scheme, scheme_name)
65
+ when 'text_box'
66
+ SvgGradientService.define_text_box_gradient(xml, color_scheme, scheme_name)
113
67
  end
114
68
  end
115
69
 
@@ -151,6 +105,7 @@ module MtgCardMaker
151
105
  ColorScheme.new(:red),
152
106
  ColorScheme.new(:green),
153
107
  ColorScheme.new(:gold),
108
+ ColorScheme.new(:silver),
154
109
  ColorScheme.new(:artifact)
155
110
  ]
156
111
  end
@@ -26,14 +26,20 @@ module MtgCardMaker
26
26
  end
27
27
 
28
28
  # Create the sprite sheet XML builder with assets and cards
29
- def create_sprite_builder(width, height, card_files, assets)
29
+ #
30
+ # @param width [Integer] the width of the sprite sheet
31
+ # @param height [Integer] the height of the sprite sheet
32
+ # @param card_files [Array<Tempfile>] the individual card files
33
+ # @param assets [SpriteSheetAssets] the assets instance
34
+ # @param embed_font [Boolean] whether to embed the font as base64 (default: true)
35
+ def create_sprite_builder(width, height, card_files, assets, embed_font: true)
30
36
  Nokogiri::XML::Builder.new do |xml|
31
37
  xml.svg(xmlns: 'http://www.w3.org/2000/svg',
32
38
  viewBox: "0 0 #{width} #{height}",
33
39
  width: width,
34
40
  height: height) do
35
- assets.add_assets_to_sprite(xml)
36
- add_cards_to_sprite(xml, card_files)
41
+ assets.add_assets_to_sprite(xml, embed_font: embed_font)
42
+ add_cards_to_sprite(xml, card_files, embed_font)
37
43
  end
38
44
  end
39
45
  end
@@ -45,13 +51,13 @@ module MtgCardMaker
45
51
 
46
52
  private
47
53
 
48
- def add_cards_to_sprite(xml, card_files)
54
+ def add_cards_to_sprite(xml, card_files, embed_font)
49
55
  card_files.each_with_index do |file, index|
50
- add_card_to_sprite(xml, file, index)
56
+ add_card_to_sprite(xml, file, index, embed_font)
51
57
  end
52
58
  end
53
59
 
54
- def add_card_to_sprite(xml, card_file, index)
60
+ def add_card_to_sprite(xml, card_file, index, embed_font)
55
61
  position = calculate_card_position(index)
56
62
  doc = load_card_document(card_file)
57
63
  svg_element = doc.at_css('svg')
@@ -59,7 +65,12 @@ module MtgCardMaker
59
65
  return unless svg_element
60
66
 
61
67
  xml.g(transform: "translate(#{position[:x]}, #{position[:y]})") do
62
- add_card_children(xml, svg_element)
68
+ # Add the card with its own viewBox for proper scaling
69
+ xml.svg(viewBox: "0 0 #{CARD_WIDTH} #{CARD_HEIGHT}",
70
+ width: CARD_WIDTH,
71
+ height: CARD_HEIGHT) do
72
+ add_card_children(xml, svg_element, embed_font)
73
+ end
63
74
  end
64
75
  end
65
76
 
@@ -77,8 +88,9 @@ module MtgCardMaker
77
88
  Nokogiri::XML(File.read(card_file.path))
78
89
  end
79
90
 
80
- def add_card_children(xml, svg_element)
91
+ def add_card_children(xml, svg_element, _embed_font)
81
92
  excluded_elements = ['defs', 'style']
93
+
82
94
  svg_element.children.each do |child|
83
95
  next if excluded_elements.include?(child.name)
84
96
  next if child.name&.start_with?('linearGradient', 'radialGradient', 'pattern')
@@ -42,11 +42,14 @@ module MtgCardMaker
42
42
  return false if card_configs.empty?
43
43
 
44
44
  begin
45
+ # Check if any card has embed_font: true
46
+ embed_font = should_embed_font?(card_configs)
47
+
45
48
  # Generate individual cards first
46
- card_files = generate_individual_cards(card_configs)
49
+ card_files = generate_individual_cards(card_configs, embed_font)
47
50
 
48
51
  # Stitch them together into a sprite sheet
49
- stitch_cards_into_sprite(card_files, output_file)
52
+ stitch_cards_into_sprite(card_files, output_file, embed_font)
50
53
  ensure
51
54
  # Clean up temporary files
52
55
  cleanup_temp_files(card_files)
@@ -63,11 +66,19 @@ module MtgCardMaker
63
66
 
64
67
  private
65
68
 
66
- def generate_individual_cards(card_configs)
69
+ def logger
70
+ @logger ||= MtgCardMaker::Logger.new
71
+ end
72
+
73
+ def should_embed_font?(card_configs)
74
+ card_configs.values.any? { |config| config['embed_font'] == true }
75
+ end
76
+
77
+ def generate_individual_cards(card_configs, embed_font)
67
78
  card_files = []
68
79
 
69
80
  card_configs.values.each_with_index do |config, index|
70
- card = card_from_config(config)
81
+ card = card_from_config(config, embed_font)
71
82
  temp_file = save_card_to_temp_file(card, index)
72
83
  card_files << temp_file
73
84
  rescue StandardError => e
@@ -82,7 +93,8 @@ module MtgCardMaker
82
93
  raise "❌ Error generating card #{index}: #{error.message}"
83
94
  end
84
95
 
85
- def card_from_config(config)
96
+ def card_from_config(config, _embed_font)
97
+ # Never embed font in individual cards when creating sprite sheets
86
98
  BaseCard.new(
87
99
  name: config['name'],
88
100
  mana_cost: config['mana_cost'],
@@ -93,7 +105,8 @@ module MtgCardMaker
93
105
  toughness: config['toughness'],
94
106
  color: config['color'],
95
107
  border_color: config['border_color'],
96
- art: config['art']
108
+ art: config['art'],
109
+ embed_font: false
97
110
  )
98
111
  end
99
112
 
@@ -103,9 +116,9 @@ module MtgCardMaker
103
116
  temp_file
104
117
  end
105
118
 
106
- def stitch_cards_into_sprite(card_files, output_file)
119
+ def stitch_cards_into_sprite(card_files, output_file, embed_font)
107
120
  width, height = sprite_dimensions(card_files.length)
108
- builder = @builder.create_sprite_builder(width, height, card_files, @assets)
121
+ builder = @builder.create_sprite_builder(width, height, card_files, @assets, embed_font: embed_font)
109
122
  @builder.write_sprite_file(output_file, builder)
110
123
  true
111
124
  rescue StandardError => e
@@ -119,7 +132,7 @@ module MtgCardMaker
119
132
  file.close
120
133
  file.unlink
121
134
  rescue StandardError => e
122
- warn "Warning: Could not clean up temp file #{file.path}: #{e.message}"
135
+ logger.warn("Could not clean up temp file #{file.path}: #{e.message}")
123
136
  end
124
137
  end
125
138
  end
@@ -3,7 +3,7 @@
3
3
  module MtgCardMaker
4
4
  # Simplified gradient definitions for SVG cards
5
5
  # Removes complex abstraction in favor of direct gradient creation
6
- class SvgGradientService # rubocop:disable Metrics/ClassLength
6
+ class SvgGradientService
7
7
  class << self
8
8
  # Defines all gradients needed for a color scheme
9
9
  def define_all_gradients(svg, color_scheme = DEFAULT_COLOR_SCHEME)
@@ -28,9 +28,9 @@ module MtgCardMaker
28
28
  "#{color_scheme.scheme_name}_name_gradient"
29
29
  end
30
30
 
31
- # Get gradient ID for description gradient
32
- def description_gradient_id(color_scheme)
33
- "#{color_scheme.scheme_name}_description_gradient"
31
+ # Get gradient ID for text box gradient
32
+ def text_box_gradient_id(color_scheme)
33
+ "#{color_scheme.scheme_name}_text_box_gradient"
34
34
  end
35
35
 
36
36
  # Get gradient ID for metallic highlight gradient
@@ -50,7 +50,7 @@ module MtgCardMaker
50
50
 
51
51
  # Check if color scheme has metallic properties
52
52
  def metallic_properties?(color_scheme)
53
- [:gold, :colorless].include?(color_scheme.scheme_name)
53
+ %i[gold colorless silver].include?(color_scheme.scheme_name)
54
54
  end
55
55
 
56
56
  def define_standard_gradients(svg, color_scheme)
@@ -59,7 +59,7 @@ module MtgCardMaker
59
59
  define_card_gradient(svg, color_scheme, scheme_name)
60
60
  define_frame_gradient(svg, color_scheme, scheme_name)
61
61
  define_name_gradient(svg, color_scheme, scheme_name)
62
- define_description_gradient(svg, color_scheme, scheme_name)
62
+ define_text_box_gradient(svg, color_scheme, scheme_name)
63
63
  end
64
64
 
65
65
  def define_card_gradient(svg, color_scheme, scheme_name)
@@ -86,11 +86,11 @@ module MtgCardMaker
86
86
  end
87
87
  end
88
88
 
89
- def define_description_gradient(svg, color_scheme, scheme_name)
90
- svg.linearGradient id: "#{scheme_name}_description_gradient", x1: '0%', y1: '0%', x2: '100%', y2: '100%' do
91
- svg.stop offset: '0%', 'stop-color': color_scheme.description_gradient_start
92
- svg.stop offset: '50%', 'stop-color': color_scheme.description_gradient_middle
93
- svg.stop offset: '100%', 'stop-color': color_scheme.description_gradient_end
89
+ def define_text_box_gradient(svg, color_scheme, scheme_name)
90
+ svg.linearGradient id: "#{scheme_name}_text_box_gradient", x1: '0%', y1: '0%', x2: '100%', y2: '100%' do
91
+ svg.stop offset: '0%', 'stop-color': color_scheme.text_box_gradient_start
92
+ svg.stop offset: '50%', 'stop-color': color_scheme.text_box_gradient_middle
93
+ svg.stop offset: '100%', 'stop-color': color_scheme.text_box_gradient_end
94
94
  end
95
95
  end
96
96