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,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# Mixin for metallic rendering logic shared by multiple layers
|
5
|
+
module MetallicRenderer
|
6
|
+
# Parameter validation and sharing class for metallic rendering
|
7
|
+
class MetallicRenderParams
|
8
|
+
attr_reader :mask, :corners, :bottom_margin, :opacity, :geometry_params
|
9
|
+
|
10
|
+
def initialize(config_params = {})
|
11
|
+
@mask = config_params[:mask]
|
12
|
+
@corners = config_params[:corners] || LayerConfig.default.corner_radius(:inner)
|
13
|
+
@bottom_margin = config_params[:bottom_margin] || 0
|
14
|
+
@opacity = config_params[:opacity] || {}
|
15
|
+
@geometry_params = config_params[:geometry] || {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def texture_opacity
|
19
|
+
opacity[:texture] || 0.05
|
20
|
+
end
|
21
|
+
|
22
|
+
def shadow_opacity
|
23
|
+
opacity[:shadow] || 0.08
|
24
|
+
end
|
25
|
+
|
26
|
+
def shadow_offset
|
27
|
+
2
|
28
|
+
end
|
29
|
+
|
30
|
+
def shadow_corners
|
31
|
+
{
|
32
|
+
x: [corners[:x] - shadow_offset, 0].max,
|
33
|
+
y: [corners[:y] - shadow_offset, 0].max
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Entrypoint for metallic rendering
|
39
|
+
def render_metallic_elements(config_params = {})
|
40
|
+
params = MetallicRenderParams.new(config_params)
|
41
|
+
|
42
|
+
render_base_frame(params)
|
43
|
+
render_texture_overlay(params)
|
44
|
+
render_shadow(params)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def render_base_frame(params)
|
50
|
+
create_rect(
|
51
|
+
params.geometry_params,
|
52
|
+
params.bottom_margin,
|
53
|
+
params.mask,
|
54
|
+
params.corners,
|
55
|
+
fill: "url(##{SvgGradientService.metallic_highlight_gradient_id(color_scheme)})",
|
56
|
+
stroke: color_scheme.primary_color,
|
57
|
+
stroke_width: LayerConfig.default.stroke_width
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_texture_overlay(params)
|
62
|
+
create_rect(
|
63
|
+
params.geometry_params,
|
64
|
+
params.bottom_margin,
|
65
|
+
params.mask,
|
66
|
+
params.corners,
|
67
|
+
fill: "url(##{SvgGradientService.metallic_pattern_id(color_scheme)})",
|
68
|
+
opacity: params.texture_opacity
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def render_shadow(params)
|
73
|
+
shadow_attrs = shadow_attributes(params)
|
74
|
+
create_rect(
|
75
|
+
params.geometry_params,
|
76
|
+
params.bottom_margin,
|
77
|
+
params.mask,
|
78
|
+
params.corners,
|
79
|
+
**shadow_attrs
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def shadow_attributes(params)
|
84
|
+
{
|
85
|
+
fill: "url(##{SvgGradientService.metallic_shadow_gradient_id(color_scheme)})",
|
86
|
+
opacity: params.shadow_opacity,
|
87
|
+
x_offset: params.shadow_offset,
|
88
|
+
y_offset: params.shadow_offset,
|
89
|
+
width_offset: params.shadow_offset * 2,
|
90
|
+
height_offset: params.shadow_offset * 2,
|
91
|
+
rx: params.shadow_corners[:x],
|
92
|
+
ry: params.shadow_corners[:y]
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_rect(geometry_params, bottom_margin, mask, corners, **attributes)
|
97
|
+
rect_attributes = build_rect_attributes(geometry_params, bottom_margin, mask, corners, attributes)
|
98
|
+
svg.rect(**rect_attributes)
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_rect_attributes(geometry_params, bottom_margin, mask, corners, attributes)
|
102
|
+
geometry = extract_geometry(geometry_params)
|
103
|
+
offsets = extract_offsets(attributes)
|
104
|
+
base_attrs = {
|
105
|
+
x: rect_x(geometry, offsets),
|
106
|
+
y: rect_y(geometry, offsets),
|
107
|
+
width: rect_width(geometry, offsets),
|
108
|
+
height: rect_height(geometry, bottom_margin, offsets),
|
109
|
+
mask: mask ? "url(##{mask})" : nil,
|
110
|
+
rx: attributes[:rx] || corners[:x],
|
111
|
+
ry: attributes[:ry] || corners[:y]
|
112
|
+
}.compact
|
113
|
+
# Merge in all other SVG attributes (fill, opacity, stroke, etc.)
|
114
|
+
base_attrs.merge!(attributes)
|
115
|
+
base_attrs
|
116
|
+
end
|
117
|
+
|
118
|
+
def rect_x(geometry, offsets)
|
119
|
+
geometry[:x] + geometry[:padding] + offsets[:x]
|
120
|
+
end
|
121
|
+
|
122
|
+
def rect_y(geometry, offsets)
|
123
|
+
geometry[:y] + geometry[:padding] + offsets[:y]
|
124
|
+
end
|
125
|
+
|
126
|
+
def rect_width(geometry, offsets)
|
127
|
+
geometry[:width] - (geometry[:padding] * 2) - offsets[:width]
|
128
|
+
end
|
129
|
+
|
130
|
+
def rect_height(geometry, bottom_margin, offsets)
|
131
|
+
geometry[:height] - bottom_margin - offsets[:height]
|
132
|
+
end
|
133
|
+
|
134
|
+
def extract_geometry(geometry_params)
|
135
|
+
layer_config = LayerConfig.default
|
136
|
+
{
|
137
|
+
x: geometry_x(geometry_params),
|
138
|
+
y: geometry_y(geometry_params),
|
139
|
+
width: geometry_width(geometry_params),
|
140
|
+
height: geometry_height(geometry_params),
|
141
|
+
padding: geometry_padding(geometry_params, layer_config)
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
def geometry_x(geometry_params)
|
146
|
+
geometry_params[:x] || (respond_to?(:x) ? x : 0)
|
147
|
+
end
|
148
|
+
|
149
|
+
def geometry_y(geometry_params)
|
150
|
+
geometry_params[:y] || (respond_to?(:y) ? y : 0)
|
151
|
+
end
|
152
|
+
|
153
|
+
def geometry_width(geometry_params)
|
154
|
+
geometry_params[:width] || (respond_to?(:width) ? width : 0)
|
155
|
+
end
|
156
|
+
|
157
|
+
def geometry_height(geometry_params)
|
158
|
+
geometry_params[:height] || (respond_to?(:height) ? height : 0)
|
159
|
+
end
|
160
|
+
|
161
|
+
def geometry_padding(geometry_params, layer_config)
|
162
|
+
geometry_params[:padding] || layer_config.horizontal_padding
|
163
|
+
end
|
164
|
+
|
165
|
+
def extract_offsets(attributes)
|
166
|
+
{
|
167
|
+
x: attributes.delete(:x_offset) || 0,
|
168
|
+
y: attributes.delete(:y_offset) || 0,
|
169
|
+
width: attributes.delete(:width_offset) || 0,
|
170
|
+
height: attributes.delete(:height_offset) || 0
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# Handles the creation of shared assets (fonts, gradients, masks) for sprite sheets
|
5
|
+
class SpriteSheetAssets
|
6
|
+
def initialize
|
7
|
+
@color_schemes = build_color_schemes
|
8
|
+
end
|
9
|
+
|
10
|
+
# Add all shared assets to the sprite sheet XML
|
11
|
+
def add_assets_to_sprite(xml)
|
12
|
+
add_sprite_styles(xml)
|
13
|
+
add_sprite_masks(xml)
|
14
|
+
add_sprite_gradients(xml)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def add_sprite_styles(xml)
|
20
|
+
xml.defs do
|
21
|
+
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
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_sprite_masks(xml)
|
77
|
+
xml.defs do
|
78
|
+
# Define the art window mask once for the entire sprite sheet
|
79
|
+
xml.mask(id: 'artWindowMask') do
|
80
|
+
# White rectangle covers the entire card (opaque)
|
81
|
+
xml.rect(x: 0, y: 0, width: '100%', height: '100%', fill: '#FFF')
|
82
|
+
# Black rectangle creates the transparent window at art position
|
83
|
+
art_config = BaseCard::DEFAULTS[:layers][:art_layer]
|
84
|
+
xml.rect(x: art_config[:x], y: art_config[:y],
|
85
|
+
width: art_config[:width], height: art_config[:height],
|
86
|
+
fill: '#000', rx: art_config[:corner_radius][:x], ry: art_config[:corner_radius][:y])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_sprite_gradients(xml)
|
92
|
+
# Define gradients for all color schemes using SvgGradientService
|
93
|
+
@color_schemes.each do |color_scheme|
|
94
|
+
SvgGradientService.define_standard_gradients(xml, color_scheme)
|
95
|
+
if SvgGradientService.metallic_properties?(color_scheme)
|
96
|
+
SvgGradientService.define_metallic_gradients(xml,
|
97
|
+
color_scheme)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Test compatibility methods - delegate to SvgGradientService
|
103
|
+
def define_gradient(xml, color_scheme, scheme_name, gradient_type, _color_methods)
|
104
|
+
case gradient_type
|
105
|
+
when 'card'
|
106
|
+
SvgGradientService.define_card_gradient(xml, color_scheme, scheme_name)
|
107
|
+
when 'frame'
|
108
|
+
SvgGradientService.define_frame_gradient(xml, color_scheme, scheme_name)
|
109
|
+
when 'name'
|
110
|
+
SvgGradientService.define_name_gradient(xml, color_scheme, scheme_name)
|
111
|
+
when 'description'
|
112
|
+
SvgGradientService.define_description_gradient(xml, color_scheme, scheme_name)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def define_metallic_gradients(xml, color_scheme, _scheme_name)
|
117
|
+
SvgGradientService.define_metallic_gradients(xml, color_scheme)
|
118
|
+
end
|
119
|
+
|
120
|
+
def define_metallic_highlight_gradient(xml, color_scheme, scheme_name)
|
121
|
+
SvgGradientService.define_metallic_highlight_gradient(xml, color_scheme, scheme_name)
|
122
|
+
end
|
123
|
+
|
124
|
+
def define_metallic_shadow_gradient(xml, color_scheme, scheme_name)
|
125
|
+
SvgGradientService.define_metallic_shadow_gradient(xml, color_scheme, scheme_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
def define_metallic_pattern(xml, color_scheme, scheme_name)
|
129
|
+
SvgGradientService.define_metallic_pattern(xml, color_scheme, scheme_name)
|
130
|
+
end
|
131
|
+
|
132
|
+
def add_metallic_pattern_lines(xml, color_scheme)
|
133
|
+
SvgGradientService.add_metallic_pattern_lines(xml, color_scheme)
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_metallic_pattern_circles(xml, color_scheme)
|
137
|
+
SvgGradientService.add_metallic_pattern_circles(xml, color_scheme)
|
138
|
+
end
|
139
|
+
|
140
|
+
def metallic_properties?(color_scheme)
|
141
|
+
SvgGradientService.metallic_properties?(color_scheme)
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_color_schemes
|
145
|
+
# Define all possible color schemes to ensure coverage
|
146
|
+
[
|
147
|
+
ColorScheme.new(:colorless),
|
148
|
+
ColorScheme.new(:white),
|
149
|
+
ColorScheme.new(:blue),
|
150
|
+
ColorScheme.new(:black),
|
151
|
+
ColorScheme.new(:red),
|
152
|
+
ColorScheme.new(:green),
|
153
|
+
ColorScheme.new(:gold),
|
154
|
+
ColorScheme.new(:artifact)
|
155
|
+
]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module MtgCardMaker
|
6
|
+
# Handles the core construction of sprite sheets from individual card files
|
7
|
+
class SpriteSheetBuilder
|
8
|
+
attr_reader :cards_per_row, :spacing
|
9
|
+
|
10
|
+
def initialize(cards_per_row: 5, spacing: 30)
|
11
|
+
@cards_per_row = cards_per_row
|
12
|
+
@spacing = spacing
|
13
|
+
end
|
14
|
+
|
15
|
+
# Calculate sprite sheet dimensions
|
16
|
+
def sprite_dimensions(card_count)
|
17
|
+
return [0, 0] if card_count.zero?
|
18
|
+
|
19
|
+
cols = [card_count, @cards_per_row].min
|
20
|
+
rows = (card_count.to_f / @cards_per_row).ceil
|
21
|
+
|
22
|
+
width = (cols * CARD_WIDTH) + ((cols - 1) * @spacing)
|
23
|
+
height = (rows * CARD_HEIGHT) + ((rows - 1) * @spacing)
|
24
|
+
|
25
|
+
[width, height]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create the sprite sheet XML builder with assets and cards
|
29
|
+
def create_sprite_builder(width, height, card_files, assets)
|
30
|
+
Nokogiri::XML::Builder.new do |xml|
|
31
|
+
xml.svg(xmlns: 'http://www.w3.org/2000/svg',
|
32
|
+
viewBox: "0 0 #{width} #{height}",
|
33
|
+
width: width,
|
34
|
+
height: height) do
|
35
|
+
assets.add_assets_to_sprite(xml)
|
36
|
+
add_cards_to_sprite(xml, card_files)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Write the sprite sheet to file
|
42
|
+
def write_sprite_file(output_file, builder)
|
43
|
+
File.write(output_file, builder.to_xml)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def add_cards_to_sprite(xml, card_files)
|
49
|
+
card_files.each_with_index do |file, index|
|
50
|
+
add_card_to_sprite(xml, file, index)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_card_to_sprite(xml, card_file, index)
|
55
|
+
position = calculate_card_position(index)
|
56
|
+
doc = load_card_document(card_file)
|
57
|
+
svg_element = doc.at_css('svg')
|
58
|
+
|
59
|
+
return unless svg_element
|
60
|
+
|
61
|
+
xml.g(transform: "translate(#{position[:x]}, #{position[:y]})") do
|
62
|
+
add_card_children(xml, svg_element)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def calculate_card_position(index)
|
67
|
+
row = index / @cards_per_row
|
68
|
+
col = index % @cards_per_row
|
69
|
+
|
70
|
+
{
|
71
|
+
x: col * (CARD_WIDTH + @spacing),
|
72
|
+
y: row * (CARD_HEIGHT + @spacing)
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_card_document(card_file)
|
77
|
+
Nokogiri::XML(File.read(card_file.path))
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_card_children(xml, svg_element)
|
81
|
+
excluded_elements = ['defs', 'style']
|
82
|
+
svg_element.children.each do |child|
|
83
|
+
next if excluded_elements.include?(child.name)
|
84
|
+
next if child.name&.start_with?('linearGradient', 'radialGradient', 'pattern')
|
85
|
+
|
86
|
+
xml.parent.add_child(child.dup)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module MtgCardMaker
|
6
|
+
# Service for creating sprite sheets by generating individual cards
|
7
|
+
# and stitching them together using SpriteSheetBuilder and SpriteSheetAssets.
|
8
|
+
# This service handles the complete workflow from card configurations to
|
9
|
+
# final sprite sheet output, including temporary file management.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# service = MtgCardMaker::SpriteSheetService.new(cards_per_row: 4, spacing: 30)
|
13
|
+
# success = service.create_sprite_sheet(card_configs, 'output.svg')
|
14
|
+
# width, height = service.sprite_dimensions(card_configs.length)
|
15
|
+
#
|
16
|
+
# @since 0.1.0
|
17
|
+
class SpriteSheetService
|
18
|
+
# @return [Integer] the number of cards per row in the sprite sheet
|
19
|
+
attr_reader :cards_per_row
|
20
|
+
|
21
|
+
# @return [Integer] the spacing between cards in pixels
|
22
|
+
attr_reader :spacing
|
23
|
+
|
24
|
+
# Initialize a new sprite sheet service
|
25
|
+
#
|
26
|
+
# @param cards_per_row [Integer] the number of cards per row (default: 5)
|
27
|
+
# @param spacing [Integer] the spacing between cards in pixels (default: 30)
|
28
|
+
def initialize(cards_per_row: 5, spacing: 30)
|
29
|
+
@cards_per_row = cards_per_row
|
30
|
+
@spacing = spacing
|
31
|
+
@builder = SpriteSheetBuilder.new(cards_per_row: cards_per_row, spacing: spacing)
|
32
|
+
@assets = SpriteSheetAssets.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create a sprite sheet from an array of card configurations
|
36
|
+
#
|
37
|
+
# @param card_configs [Hash] the card configurations
|
38
|
+
# @param output_file [String] the output file path
|
39
|
+
# @return [Boolean] true if successful, false if no cards provided
|
40
|
+
# @raise [StandardError] if card generation or sprite creation fails
|
41
|
+
def create_sprite_sheet(card_configs, output_file)
|
42
|
+
return false if card_configs.empty?
|
43
|
+
|
44
|
+
begin
|
45
|
+
# Generate individual cards first
|
46
|
+
card_files = generate_individual_cards(card_configs)
|
47
|
+
|
48
|
+
# Stitch them together into a sprite sheet
|
49
|
+
stitch_cards_into_sprite(card_files, output_file)
|
50
|
+
ensure
|
51
|
+
# Clean up temporary files
|
52
|
+
cleanup_temp_files(card_files)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Calculate sprite sheet dimensions for a given number of cards
|
57
|
+
#
|
58
|
+
# @param card_count [Integer] the number of cards in the sprite sheet
|
59
|
+
# @return [Array<Integer>] the width and height of the sprite sheet
|
60
|
+
def sprite_dimensions(card_count)
|
61
|
+
@builder.sprite_dimensions(card_count)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def generate_individual_cards(card_configs)
|
67
|
+
card_files = []
|
68
|
+
|
69
|
+
card_configs.values.each_with_index do |config, index|
|
70
|
+
card = card_from_config(config)
|
71
|
+
temp_file = save_card_to_temp_file(card, index)
|
72
|
+
card_files << temp_file
|
73
|
+
rescue StandardError => e
|
74
|
+
handle_card_generation_error(e, index, card_files)
|
75
|
+
end
|
76
|
+
|
77
|
+
card_files
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_card_generation_error(error, index, card_files)
|
81
|
+
cleanup_temp_files(card_files)
|
82
|
+
raise "❌ Error generating card #{index}: #{error.message}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def card_from_config(config)
|
86
|
+
BaseCard.new(
|
87
|
+
name: config['name'],
|
88
|
+
mana_cost: config['mana_cost'],
|
89
|
+
type_line: config['type_line'],
|
90
|
+
rules_text: config['rules_text'],
|
91
|
+
flavor_text: config['flavor_text'],
|
92
|
+
power: config['power'],
|
93
|
+
toughness: config['toughness'],
|
94
|
+
color: config['color'],
|
95
|
+
border_color: config['border_color'],
|
96
|
+
art: config['art']
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
def save_card_to_temp_file(card, index)
|
101
|
+
temp_file = Tempfile.new(["card_#{index}", '.svg'])
|
102
|
+
card.save(temp_file.path)
|
103
|
+
temp_file
|
104
|
+
end
|
105
|
+
|
106
|
+
def stitch_cards_into_sprite(card_files, output_file)
|
107
|
+
width, height = sprite_dimensions(card_files.length)
|
108
|
+
builder = @builder.create_sprite_builder(width, height, card_files, @assets)
|
109
|
+
@builder.write_sprite_file(output_file, builder)
|
110
|
+
true
|
111
|
+
rescue StandardError => e
|
112
|
+
raise "❌ Error creating sprite sheet: #{e.message}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def cleanup_temp_files(card_files)
|
116
|
+
return if card_files.nil?
|
117
|
+
|
118
|
+
card_files.each do |file|
|
119
|
+
file.close
|
120
|
+
file.unlink
|
121
|
+
rescue StandardError => e
|
122
|
+
warn "Warning: Could not clean up temp file #{file.path}: #{e.message}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|