mtg_card_maker 0.1.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/CODE_OF_CONDUCT.md +131 -0
- data/LICENSE.txt +20 -0
- data/README.md +305 -0
- data/bin/mtg_card_maker +7 -0
- data/lib/mtg_card_maker/base_card.rb +210 -0
- data/lib/mtg_card_maker/cli.rb +221 -0
- data/lib/mtg_card_maker/color_palette.rb +135 -0
- data/lib/mtg_card_maker/color_scheme.rb +305 -0
- data/lib/mtg_card_maker/core_ext/deep_merge.rb +21 -0
- data/lib/mtg_card_maker/fonts/Goudy Mediaeval DemiBold.ttf +0 -0
- data/lib/mtg_card_maker/fonts/goudy_base64.txt +1 -0
- data/lib/mtg_card_maker/icon_service.rb +95 -0
- data/lib/mtg_card_maker/icons/black.svg +1 -0
- data/lib/mtg_card_maker/icons/blue.svg +1 -0
- data/lib/mtg_card_maker/icons/colorless.svg +1 -0
- data/lib/mtg_card_maker/icons/green.svg +1 -0
- data/lib/mtg_card_maker/icons/jsharp.svg +1 -0
- data/lib/mtg_card_maker/icons/qrcode.svg +1 -0
- data/lib/mtg_card_maker/icons/red.svg +1 -0
- data/lib/mtg_card_maker/icons/white.svg +1 -0
- data/lib/mtg_card_maker/layer_config.rb +289 -0
- data/lib/mtg_card_maker/layer_factory.rb +122 -0
- data/lib/mtg_card_maker/layer_initializer.rb +12 -0
- data/lib/mtg_card_maker/layers/art_layer.rb +63 -0
- data/lib/mtg_card_maker/layers/border_layer.rb +166 -0
- data/lib/mtg_card_maker/layers/frame_layer.rb +62 -0
- data/lib/mtg_card_maker/layers/name_layer.rb +82 -0
- data/lib/mtg_card_maker/layers/power_layer.rb +69 -0
- data/lib/mtg_card_maker/layers/text_box_layer.rb +107 -0
- data/lib/mtg_card_maker/layers/type_line_layer.rb +86 -0
- data/lib/mtg_card_maker/mana_cost.rb +220 -0
- data/lib/mtg_card_maker/metallic_renderer.rb +174 -0
- data/lib/mtg_card_maker/sprite_sheet_assets.rb +158 -0
- data/lib/mtg_card_maker/sprite_sheet_builder.rb +90 -0
- data/lib/mtg_card_maker/sprite_sheet_service.rb +126 -0
- data/lib/mtg_card_maker/svg_gradient_service.rb +159 -0
- data/lib/mtg_card_maker/text_rendering_service.rb +160 -0
- data/lib/mtg_card_maker/version.rb +8 -0
- data/lib/mtg_card_maker.rb +268 -0
- data/sig/mtg_card_maker.rbs +4 -0
- metadata +149 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../mana_cost'
|
4
|
+
|
5
|
+
module MtgCardMaker
|
6
|
+
# NameLayer is a specialized layer
|
7
|
+
# This is the area that surrounds the name and cost of the card
|
8
|
+
class NameLayer < BaseLayer
|
9
|
+
include LayerInitializer
|
10
|
+
attr_reader :name, :cost, :color_scheme
|
11
|
+
|
12
|
+
def initialize(dimensions:, name:, cost:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
|
13
|
+
frame_color = initialize_layer_color(color, color_scheme, :background_color)
|
14
|
+
super(dimensions: dimensions, color: frame_color)
|
15
|
+
@name = name
|
16
|
+
@cost = cost
|
17
|
+
@color_scheme = color_scheme
|
18
|
+
end
|
19
|
+
|
20
|
+
def render
|
21
|
+
# Ensure gradients are defined for this color scheme
|
22
|
+
SvgGradientService.define_all_gradients(svg, color_scheme)
|
23
|
+
|
24
|
+
svg.g do
|
25
|
+
render_name_area
|
26
|
+
render_card_name
|
27
|
+
render_mana_cost if cost && !cost.empty?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def render_name_area
|
34
|
+
layer_config = LayerConfig.default
|
35
|
+
stroke_width = layer_config.stroke_width
|
36
|
+
corners = layer_config.corner_radius(:name)
|
37
|
+
|
38
|
+
svg.rect x: x, y: y, width: width, height: height,
|
39
|
+
fill: "url(##{SvgGradientService.name_gradient_id(color_scheme)})",
|
40
|
+
stroke: ColorPalette::FRAME_STROKE_COLOR,
|
41
|
+
stroke_width: stroke_width,
|
42
|
+
rx: corners[:x], ry: corners[:y], mask: "url(##{@mask_id})"
|
43
|
+
end
|
44
|
+
|
45
|
+
def render_card_name
|
46
|
+
layer_config = LayerConfig.default
|
47
|
+
text_service = TextRenderingService.new(
|
48
|
+
text: name,
|
49
|
+
layer_config: layer_config,
|
50
|
+
x: layer_config.text_x_position(x),
|
51
|
+
y: layer_config.text_y_position(y, :name_area, height),
|
52
|
+
font_size: layer_config.font_size(:name),
|
53
|
+
available_width: layer_config.text_width(width, :name_area),
|
54
|
+
css_class: layer_config.css_class(:card_name)
|
55
|
+
)
|
56
|
+
lines = text_service.wrapped_text_lines
|
57
|
+
lines.each do |line, attrs|
|
58
|
+
svg.text line, attrs
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def render_mana_cost
|
63
|
+
mana_cost = ManaCost.new(cost)
|
64
|
+
cost_x = cost_position_x(mana_cost)
|
65
|
+
cost_y = cost_position_y
|
66
|
+
svg.g transform: "translate(#{cost_x}, #{cost_y})" do
|
67
|
+
svg << mana_cost.to_svg
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def cost_position_x(mana_cost)
|
72
|
+
layer_config = LayerConfig.default
|
73
|
+
config = layer_config.mana_cost_config
|
74
|
+
base_x = x + width - config[:margin] - config[:circle_radius]
|
75
|
+
base_x - (config[:circle_spacing] * (mana_cost.elements.length - 1))
|
76
|
+
end
|
77
|
+
|
78
|
+
def cost_position_y
|
79
|
+
y + (height / 2) - 2
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# PowerLayer is a specialized layer
|
5
|
+
# This is the area that surrounds the power and toughness of the card
|
6
|
+
class PowerLayer < BaseLayer
|
7
|
+
include LayerInitializer
|
8
|
+
attr_reader :power, :toughness, :color_scheme
|
9
|
+
|
10
|
+
def initialize(dimensions:, power:, toughness:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
|
11
|
+
frame_color = initialize_layer_color(color, color_scheme, :background_color)
|
12
|
+
super(dimensions: dimensions, color: frame_color)
|
13
|
+
@power = power
|
14
|
+
@toughness = toughness
|
15
|
+
@color_scheme = color_scheme
|
16
|
+
end
|
17
|
+
|
18
|
+
def render
|
19
|
+
# Don't render if power or toughness are nil, empty, or invalid
|
20
|
+
return if power.nil? || toughness.nil? || power.to_s.strip.empty? || toughness.to_s.strip.empty?
|
21
|
+
|
22
|
+
# Ensure gradients are defined for this color scheme
|
23
|
+
SvgGradientService.define_all_gradients(svg, color_scheme)
|
24
|
+
|
25
|
+
layer_config = LayerConfig.default
|
26
|
+
stroke_width = layer_config.stroke_width
|
27
|
+
corners = layer_config.corner_radius(:power)
|
28
|
+
|
29
|
+
svg.g do
|
30
|
+
# P/T box with ornate frame
|
31
|
+
svg.rect x: x_position, y: y, width: dynamic_width, height: height,
|
32
|
+
fill: "url(##{SvgGradientService.name_gradient_id(color_scheme)})",
|
33
|
+
stroke: ColorPalette::FRAME_STROKE_COLOR,
|
34
|
+
stroke_width: stroke_width, rx: corners[:x], ry: corners[:y]
|
35
|
+
|
36
|
+
# P/T text with bold styling
|
37
|
+
cost_attrs = {
|
38
|
+
x: x_position + (dynamic_width / 2),
|
39
|
+
y: y + (height / 2) + layer_config.positioning(:power_area)[:y_offset],
|
40
|
+
fill: DEFAULT_TEXT_COLOR,
|
41
|
+
font_size: layer_config.font_size(:power_area),
|
42
|
+
text_anchor: 'middle',
|
43
|
+
class: 'card-power-toughness'
|
44
|
+
}
|
45
|
+
svg.text "#{power}/#{toughness}", cost_attrs
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def dynamic_width
|
52
|
+
# Calculate total character count: power + toughness + "/"
|
53
|
+
total_chars = power.to_s.length + toughness.to_s.length + 1
|
54
|
+
|
55
|
+
# Base width for 3 characters, with steady increase per additional character
|
56
|
+
base_width = 60
|
57
|
+
width_per_char = 13
|
58
|
+
base_width + ((total_chars - 3) * width_per_char)
|
59
|
+
end
|
60
|
+
|
61
|
+
def x_position
|
62
|
+
# Anchor right side to card width (630px) with 35px margin
|
63
|
+
# Right edge should be at 595px (630 - 35)
|
64
|
+
margin = 35
|
65
|
+
right_edge = CARD_WIDTH - margin
|
66
|
+
right_edge - dynamic_width
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# TextBoxLayer is a specialized layer for the rules and flavor text
|
5
|
+
class TextBoxLayer < BaseLayer
|
6
|
+
include LayerInitializer
|
7
|
+
attr_reader :rules_text, :flavor_text, :color_scheme
|
8
|
+
|
9
|
+
def initialize(dimensions:, rules_text:, flavor_text: nil, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
|
10
|
+
frame_color = initialize_layer_color(color, color_scheme, :background_color)
|
11
|
+
super(dimensions: dimensions, color: frame_color)
|
12
|
+
@rules_text = rules_text
|
13
|
+
@flavor_text = flavor_text
|
14
|
+
@color_scheme = color_scheme
|
15
|
+
end
|
16
|
+
|
17
|
+
# Render the rules text and flavor text in a text box with a background and highlight
|
18
|
+
def render
|
19
|
+
# Ensure gradients are defined for this color scheme
|
20
|
+
SvgGradientService.define_all_gradients(svg, color_scheme)
|
21
|
+
|
22
|
+
render_text_box
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def render_text_box
|
28
|
+
layer_config = LayerConfig.default
|
29
|
+
stroke_width = layer_config.stroke_width
|
30
|
+
|
31
|
+
svg.g do
|
32
|
+
# Text box background with mask for transparent window
|
33
|
+
svg.rect x: x, y: y, width: width, height: height,
|
34
|
+
fill: "url(##{SvgGradientService.description_gradient_id(color_scheme)})",
|
35
|
+
stroke: color_scheme.primary_color,
|
36
|
+
stroke_width: stroke_width
|
37
|
+
|
38
|
+
render_text_content
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def render_text_content
|
43
|
+
render_rules_text
|
44
|
+
render_flavor_text if flavor_text
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_rules_text
|
48
|
+
layer_config = LayerConfig.default
|
49
|
+
text_service = create_text_service(layer_config, rules_text, :description, :card_description)
|
50
|
+
render_text_lines(text_service)
|
51
|
+
end
|
52
|
+
|
53
|
+
def render_flavor_text
|
54
|
+
return unless flavor_text && !flavor_text.strip.empty?
|
55
|
+
|
56
|
+
render_flavor_separator
|
57
|
+
layer_config = LayerConfig.default
|
58
|
+
text_service = create_text_service(layer_config, flavor_text, :flavor_text, :flavor_text)
|
59
|
+
render_text_lines(text_service)
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_text_service(layer_config, text, text_type, css_class)
|
63
|
+
TextRenderingService.new(
|
64
|
+
text: text,
|
65
|
+
layer_config: layer_config,
|
66
|
+
x: layer_config.text_x_position(x),
|
67
|
+
y: calculate_text_y_position(layer_config, text_type),
|
68
|
+
font_size: layer_config.font_size(text_type),
|
69
|
+
color: color_scheme.text_color,
|
70
|
+
available_width: layer_config.text_width(width, text_type),
|
71
|
+
css_class: layer_config.css_class(css_class)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def calculate_text_y_position(layer_config, text_type)
|
76
|
+
if text_type == :flavor_text
|
77
|
+
y + height - layer_config.positioning(:flavor_text)[:y_offset]
|
78
|
+
else
|
79
|
+
layer_config.text_y_position(y, :description)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def render_text_lines(text_service)
|
84
|
+
text_service.wrapped_text_lines.each do |line, attrs|
|
85
|
+
svg.text line, attrs
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def render_flavor_separator
|
90
|
+
svg.line(**separator_config)
|
91
|
+
end
|
92
|
+
|
93
|
+
def separator_config
|
94
|
+
layer_config = LayerConfig.default
|
95
|
+
separator_offset = layer_config.positioning(:flavor_text)[:separator_offset]
|
96
|
+
separator_y = y + height - separator_offset
|
97
|
+
{
|
98
|
+
x1: layer_config.text_x_position(x),
|
99
|
+
y1: separator_y,
|
100
|
+
x2: x + width - layer_config.horizontal_padding,
|
101
|
+
y2: separator_y,
|
102
|
+
stroke: color_scheme.primary_color,
|
103
|
+
stroke_width: 1
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# TypeLineLayer is a specialized layer for the type of the card
|
5
|
+
# This is the small area in the center of the card below the artwork
|
6
|
+
class TypeLineLayer < BaseLayer
|
7
|
+
include LayerInitializer
|
8
|
+
attr_reader :type_line, :color_scheme
|
9
|
+
|
10
|
+
def initialize(dimensions:, type_line:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
|
11
|
+
frame_color = initialize_layer_color(color, color_scheme, :background_color)
|
12
|
+
super(dimensions: dimensions, color: frame_color)
|
13
|
+
@type_line = type_line
|
14
|
+
@color_scheme = color_scheme
|
15
|
+
end
|
16
|
+
|
17
|
+
def render
|
18
|
+
# Ensure gradients are defined for this color scheme
|
19
|
+
SvgGradientService.define_all_gradients(svg, color_scheme)
|
20
|
+
|
21
|
+
svg.g do
|
22
|
+
render_type_background
|
23
|
+
render_type_line
|
24
|
+
render_type_icon
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def render_type_background
|
31
|
+
layer_config = LayerConfig.default
|
32
|
+
stroke_width = layer_config.stroke_width
|
33
|
+
corners = layer_config.corner_radius(:type)
|
34
|
+
|
35
|
+
svg.rect x: x, y: y, width: width, height: height,
|
36
|
+
fill: "url(##{color_scheme.scheme_name}_name_gradient)",
|
37
|
+
stroke: ColorPalette::FRAME_STROKE_COLOR,
|
38
|
+
stroke_width: stroke_width,
|
39
|
+
rx: corners[:x], ry: corners[:y]
|
40
|
+
end
|
41
|
+
|
42
|
+
def render_type_icon
|
43
|
+
layer_config = LayerConfig.default
|
44
|
+
icon_config = layer_config.type_icon_config
|
45
|
+
|
46
|
+
svg.path stroke_width: 3,
|
47
|
+
d: jsharp_path_data,
|
48
|
+
fill: 'none',
|
49
|
+
stroke: ColorPalette::FRAME_STROKE_COLOR,
|
50
|
+
transform: build_icon_transform(icon_config),
|
51
|
+
'aria-label': 'J#'
|
52
|
+
end
|
53
|
+
|
54
|
+
def jsharp_path_data
|
55
|
+
jsharp_svg = IconService.new.jsharp_svg
|
56
|
+
raise 'J# icon not found' unless jsharp_svg
|
57
|
+
|
58
|
+
path_match = jsharp_svg.match(/<path[^>]*d="([^"]*)"[^>]*>/)
|
59
|
+
path_match&.[](1)
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_icon_transform(icon_config)
|
63
|
+
translate_part = "translate(#{width - icon_config[:x_offset]},#{y + icon_config[:y_offset]})"
|
64
|
+
scale_part = "scale(#{icon_config[:scale]})"
|
65
|
+
aspect_part = "scale(#{icon_config[:aspect_ratio][:x]},#{icon_config[:aspect_ratio][:y]})"
|
66
|
+
|
67
|
+
"#{translate_part} #{scale_part} #{aspect_part}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def render_type_line
|
71
|
+
layer_config = LayerConfig.default
|
72
|
+
text_service = TextRenderingService.new(
|
73
|
+
text: type_line,
|
74
|
+
layer_config: layer_config,
|
75
|
+
x: layer_config.text_x_position(x),
|
76
|
+
y: layer_config.text_y_position(y, :type_area, height),
|
77
|
+
font_size: layer_config.font_size(:type),
|
78
|
+
available_width: layer_config.text_width(width, :type_area),
|
79
|
+
css_class: layer_config.css_class(:card_type)
|
80
|
+
)
|
81
|
+
text_service.wrapped_text_lines.each do |line, attrs|
|
82
|
+
svg.text line, attrs
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'icon_service'
|
4
|
+
|
5
|
+
module MtgCardMaker
|
6
|
+
# Mana cost class that generates SVG for the mana cost of a card.
|
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.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# mana_cost = MtgCardMaker::ManaCost.new("2UR")
|
12
|
+
# svg = mana_cost.to_svg
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# mana_cost = MtgCardMaker::ManaCost.new("XG", icon_set: :custom)
|
16
|
+
# svg = mana_cost.to_svg
|
17
|
+
#
|
18
|
+
# @since 0.1.0
|
19
|
+
# rubocop:disable Metrics/ClassLength
|
20
|
+
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
|
33
|
+
attr_reader :elements
|
34
|
+
|
35
|
+
# @return [Integer, nil] the integer value for generic mana
|
36
|
+
attr_reader :int_val
|
37
|
+
|
38
|
+
# Initialize a new mana cost parser
|
39
|
+
#
|
40
|
+
# @param mana_string [String, nil] the mana cost string (e.g., "2UR", "XG")
|
41
|
+
# @param icon_set [Symbol] the icon set to use (default: :default)
|
42
|
+
def initialize(mana_string = nil, icon_set = :default) # rubocop:disable Metrics/MethodLength
|
43
|
+
@elements = []
|
44
|
+
@origins = [] # :numeric, :C, or color symbol
|
45
|
+
@int_val = nil
|
46
|
+
@original_string = mana_string
|
47
|
+
@icon_service = IconService.new(icon_set)
|
48
|
+
|
49
|
+
return if mana_string.nil? || mana_string.empty?
|
50
|
+
|
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)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns SVG string for the mana cost circles with drop shadow
|
65
|
+
#
|
66
|
+
# @return [String] the SVG markup for the mana cost
|
67
|
+
def to_svg
|
68
|
+
layer_config = LayerConfig.default
|
69
|
+
circle_spacing = layer_config.mana_cost_config[:circle_spacing]
|
70
|
+
|
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])
|
76
|
+
end.join
|
77
|
+
|
78
|
+
<<~SVG.delete("\n")
|
79
|
+
#{drop_shadow_filter}
|
80
|
+
<g filter="url(#mana-cost-drop-shadow)">
|
81
|
+
#{mana_circles}
|
82
|
+
</g>
|
83
|
+
SVG
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def parse_mana_string(mana_string)
|
89
|
+
return if mana_string.nil?
|
90
|
+
|
91
|
+
if numeric_cost?(mana_string) || mana_string.start_with?('X')
|
92
|
+
parse_numeric_cost(mana_string)
|
93
|
+
else
|
94
|
+
parse_colored_mana(mana_string)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def numeric_cost?(mana_string)
|
99
|
+
mana_string.match?(/^\d+/)
|
100
|
+
end
|
101
|
+
|
102
|
+
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
|
107
|
+
|
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
|
113
|
+
|
114
|
+
# Remove the integer and parse remaining colored mana
|
115
|
+
remaining = mana_string[int_match[1].length..]
|
116
|
+
parse_colored_mana(remaining)
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse_colored_mana(mana_string)
|
120
|
+
return if mana_string.nil? || mana_string.empty?
|
121
|
+
|
122
|
+
mana_string.chars.each do |char|
|
123
|
+
process_colored_character(char)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
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
|
136
|
+
end
|
137
|
+
|
138
|
+
def colorless_symbol?(color, char)
|
139
|
+
color == :colorless && char == 'C'
|
140
|
+
end
|
141
|
+
|
142
|
+
def drop_shadow_filter
|
143
|
+
layer_config = LayerConfig.default
|
144
|
+
drop_shadow = layer_config.drop_shadow_config
|
145
|
+
<<~SVG.delete("\n")
|
146
|
+
<defs>
|
147
|
+
<filter id="mana-cost-drop-shadow"
|
148
|
+
x="-50%"
|
149
|
+
y="-50%"
|
150
|
+
width="200%"
|
151
|
+
height="200%">
|
152
|
+
<feDropShadow dx="#{drop_shadow[:dx]}"
|
153
|
+
dy="#{drop_shadow[:dy]}"
|
154
|
+
stdDeviation="#{drop_shadow[:std_deviation]}"
|
155
|
+
flood-color="black"
|
156
|
+
flood-opacity="#{drop_shadow[:flood_opacity]}"/>
|
157
|
+
</filter>
|
158
|
+
</defs>
|
159
|
+
SVG
|
160
|
+
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
|
+
end
|
219
|
+
# rubocop:enable Metrics/ClassLength
|
220
|
+
end
|