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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +59 -14
- data/README.md +251 -69
- data/lib/mtg_card_maker/base_card.rb +24 -1
- data/lib/mtg_card_maker/cli.rb +44 -41
- data/lib/mtg_card_maker/color_scheme.rb +18 -23
- data/lib/mtg_card_maker/css_service.rb +72 -0
- data/lib/mtg_card_maker/fonts/GoudyBookletter1911.ttf +0 -0
- data/lib/mtg_card_maker/fonts/OFL.txt +92 -0
- data/lib/mtg_card_maker/fonts/goudy_bookletter_base64.txt +1 -0
- data/lib/mtg_card_maker/icon_filename_service.rb +118 -0
- data/lib/mtg_card_maker/icon_service.rb +87 -25
- data/lib/mtg_card_maker/icons/black.svg +6 -1
- data/lib/mtg_card_maker/icons/blue.svg +6 -1
- data/lib/mtg_card_maker/icons/colorless.svg +6 -1
- data/lib/mtg_card_maker/icons/double-digit.svg +6 -0
- data/lib/mtg_card_maker/icons/energy.svg +26 -0
- data/lib/mtg_card_maker/icons/green.svg +6 -1
- data/lib/mtg_card_maker/icons/hybrid/2-black.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/2-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/2-green.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/2-red.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/2-white.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/black-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/black-green.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/black-red.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/black-white.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/blue-black.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/blue-green.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/blue-red.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/blue-white.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/colorless-black.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/colorless-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/colorless-green.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/colorless-red.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/colorless-white.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/green-black.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/green-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/green-red.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/green-white.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/red-black.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/red-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/red-green.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/red-white.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/white-black.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/white-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/white-green.svg +19 -0
- data/lib/mtg_card_maker/icons/hybrid/white-red.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/black-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/black-green.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/black-red.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/black-white.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/black.svg +6 -0
- data/lib/mtg_card_maker/icons/phyrexian/blue-black.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/blue-green.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/blue-red.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/blue-white.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/blue.svg +6 -0
- data/lib/mtg_card_maker/icons/phyrexian/colorless.svg +6 -0
- data/lib/mtg_card_maker/icons/phyrexian/green-black.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/green-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/green-red.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/green-white.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/green.svg +6 -0
- data/lib/mtg_card_maker/icons/phyrexian/red-black.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/red-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/red-green.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/red-white.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/red.svg +6 -0
- data/lib/mtg_card_maker/icons/phyrexian/white-black.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/white-blue.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/white-green.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/white-red.svg +19 -0
- data/lib/mtg_card_maker/icons/phyrexian/white.svg +6 -0
- data/lib/mtg_card_maker/icons/red.svg +6 -1
- data/lib/mtg_card_maker/icons/single-digit.svg +6 -0
- data/lib/mtg_card_maker/icons/snow.svg +24 -0
- data/lib/mtg_card_maker/icons/tap.svg +1 -0
- data/lib/mtg_card_maker/icons/tap_legacy.svg +45 -0
- data/lib/mtg_card_maker/icons/untap.svg +1 -0
- data/lib/mtg_card_maker/icons/white.svg +6 -1
- data/lib/mtg_card_maker/icons/x.svg +6 -0
- data/lib/mtg_card_maker/layer_config.rb +21 -46
- data/lib/mtg_card_maker/layers/border_layer.rb +4 -5
- data/lib/mtg_card_maker/layers/frame_layer.rb +1 -1
- data/lib/mtg_card_maker/layers/name_layer.rb +16 -8
- data/lib/mtg_card_maker/layers/power_layer.rb +1 -3
- data/lib/mtg_card_maker/layers/text_box_layer.rb +205 -51
- data/lib/mtg_card_maker/layers/type_line_layer.rb +2 -4
- data/lib/mtg_card_maker/logger.rb +62 -0
- data/lib/mtg_card_maker/mana_cost.rb +59 -128
- data/lib/mtg_card_maker/sprite_sheet_assets.rb +10 -55
- data/lib/mtg_card_maker/sprite_sheet_builder.rb +20 -8
- data/lib/mtg_card_maker/sprite_sheet_service.rb +22 -9
- data/lib/mtg_card_maker/svg_gradient_service.rb +11 -11
- data/lib/mtg_card_maker/symbol_replacement_service.rb +102 -0
- data/lib/mtg_card_maker/text_rendering_service.rb +7 -3
- data/lib/mtg_card_maker/version.rb +1 -1
- data/lib/mtg_card_maker.rb +9 -78
- metadata +79 -12
- data/lib/mtg_card_maker/fonts/Goudy Mediaeval DemiBold.ttf +0 -0
- 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(:
|
|
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
|
|
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
|
-
#
|
|
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)
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
#{
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
107
|
+
# Allow any symbol - IconService will handle validation
|
|
108
|
+
@elements << formatted_symbol
|
|
124
109
|
end
|
|
125
110
|
end
|
|
126
111
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
139
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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 '
|
|
112
|
-
SvgGradientService.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
32
|
-
def
|
|
33
|
-
"#{color_scheme.scheme_name}
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
90
|
-
svg.linearGradient id: "#{scheme_name}
|
|
91
|
-
svg.stop offset: '0%', 'stop-color': color_scheme.
|
|
92
|
-
svg.stop offset: '50%', 'stop-color': color_scheme.
|
|
93
|
-
svg.stop offset: '100%', 'stop-color': color_scheme.
|
|
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
|
|