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,289 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'core_ext/deep_merge'
|
4
|
+
|
5
|
+
module MtgCardMaker
|
6
|
+
# Configuration class for layer-specific styling and positioning that centralizes
|
7
|
+
# all hardcoded values used across different layers. This class provides a
|
8
|
+
# centralized configuration system for font sizes, positioning, frame styling,
|
9
|
+
# and other layer-specific settings.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# config = MtgCardMaker::LayerConfig.new
|
13
|
+
# config.font_size(:name) # => 32
|
14
|
+
# config.corner_radius(:outer) # => { x: 25, y: 25 }
|
15
|
+
#
|
16
|
+
# @example Custom configuration
|
17
|
+
# custom_config = {
|
18
|
+
# font_sizes: { name: 36, type: 24 },
|
19
|
+
# padding: { horizontal: 20 }
|
20
|
+
# }
|
21
|
+
# config = MtgCardMaker::LayerConfig.new(custom_config)
|
22
|
+
#
|
23
|
+
# @since 0.1.0
|
24
|
+
class LayerConfig
|
25
|
+
# Default configuration for all layers
|
26
|
+
# @return [Hash] the default configuration hash
|
27
|
+
DEFAULT_CONFIG = {
|
28
|
+
# Text rendering settings
|
29
|
+
font_sizes: {
|
30
|
+
name: 32,
|
31
|
+
type: 22,
|
32
|
+
description: 24,
|
33
|
+
flavor_text: 18,
|
34
|
+
power_area: 28,
|
35
|
+
copyright: 14
|
36
|
+
},
|
37
|
+
|
38
|
+
# Text rendering configuration
|
39
|
+
text_rendering: {
|
40
|
+
default_font_size: 16,
|
41
|
+
default_color: '#111',
|
42
|
+
default_line_height_multiplier: 1.2,
|
43
|
+
char_width_multiplier: 0.42,
|
44
|
+
css_classes: {
|
45
|
+
card_name: 'card-name',
|
46
|
+
card_type: 'card-type',
|
47
|
+
card_description: 'card-description',
|
48
|
+
flavor_text: 'card-flavor-text',
|
49
|
+
power_area: 'card-power-toughness',
|
50
|
+
copyright: 'card-copyright'
|
51
|
+
}
|
52
|
+
},
|
53
|
+
|
54
|
+
# Positioning and spacing
|
55
|
+
padding: {
|
56
|
+
horizontal: 15,
|
57
|
+
vertical: 30
|
58
|
+
},
|
59
|
+
|
60
|
+
# Layer-specific positioning offsets
|
61
|
+
positioning: {
|
62
|
+
name_area: {
|
63
|
+
y_offset: 10,
|
64
|
+
width_ratio: 0.75
|
65
|
+
},
|
66
|
+
type_area: {
|
67
|
+
y_offset: 8,
|
68
|
+
width_ratio: 0.75
|
69
|
+
},
|
70
|
+
description: {
|
71
|
+
y_offset: 30,
|
72
|
+
width_ratio: 1.0
|
73
|
+
},
|
74
|
+
flavor_text: {
|
75
|
+
y_offset: 45,
|
76
|
+
width_ratio: 1.0,
|
77
|
+
separator_offset: 70
|
78
|
+
},
|
79
|
+
power_area: {
|
80
|
+
y_offset: 9
|
81
|
+
}
|
82
|
+
},
|
83
|
+
|
84
|
+
# Frame styling
|
85
|
+
frames: {
|
86
|
+
stroke_width: 2,
|
87
|
+
corner_radius: {
|
88
|
+
name: { x: 10, y: 25 },
|
89
|
+
type: { x: 10, y: 25 },
|
90
|
+
power: { x: 10, y: 25 },
|
91
|
+
art: { x: 8, y: 8 },
|
92
|
+
inner: { x: 10, y: 10 },
|
93
|
+
outer: { x: 25, y: 25 }
|
94
|
+
}
|
95
|
+
},
|
96
|
+
|
97
|
+
# Mana cost settings
|
98
|
+
mana_cost: {
|
99
|
+
circle_radius: 15,
|
100
|
+
circle_spacing: 35,
|
101
|
+
icon_size: 24,
|
102
|
+
max_circles: 10,
|
103
|
+
margin: 10
|
104
|
+
},
|
105
|
+
|
106
|
+
# Copyright text settings
|
107
|
+
copyright: {
|
108
|
+
base_y: 830,
|
109
|
+
line_spacing: 18,
|
110
|
+
x_position: 90
|
111
|
+
},
|
112
|
+
|
113
|
+
# Type line icon settings
|
114
|
+
type_icon: {
|
115
|
+
x_offset: 13,
|
116
|
+
y_offset: 4,
|
117
|
+
scale: 0.23,
|
118
|
+
aspect_ratio: { x: 0.93839063, y: 1.0656543 }
|
119
|
+
},
|
120
|
+
|
121
|
+
# Frame settings
|
122
|
+
frame: {
|
123
|
+
bottom_margin: 120
|
124
|
+
},
|
125
|
+
|
126
|
+
# Drop shadow settings
|
127
|
+
drop_shadow: {
|
128
|
+
dx: -2,
|
129
|
+
dy: 2,
|
130
|
+
std_deviation: 1,
|
131
|
+
flood_opacity: 1.0
|
132
|
+
},
|
133
|
+
|
134
|
+
# Text positioning adjustments
|
135
|
+
text_positioning: {
|
136
|
+
mana_cost_text_y_offset: 7,
|
137
|
+
mana_cost_font_size: 24
|
138
|
+
},
|
139
|
+
|
140
|
+
# Icon opacity settings
|
141
|
+
icon_opacity: {
|
142
|
+
mana_cost: 0.7
|
143
|
+
}
|
144
|
+
}.freeze
|
145
|
+
|
146
|
+
# @return [Hash] the merged configuration hash
|
147
|
+
attr_reader :config
|
148
|
+
|
149
|
+
# Initialize a new layer configuration
|
150
|
+
#
|
151
|
+
# @param custom_config [Hash] custom configuration to merge with defaults
|
152
|
+
def initialize(custom_config = {})
|
153
|
+
# Extend the DEFAULT_CONFIG hash with deep merge functionality
|
154
|
+
default_config_with_merge = DEFAULT_CONFIG.dup.extend(DeepMerge)
|
155
|
+
@config = default_config_with_merge.deep_merge(custom_config)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Font size getters
|
159
|
+
def font_size(layer_type)
|
160
|
+
config[:font_sizes][layer_type]
|
161
|
+
end
|
162
|
+
|
163
|
+
# Padding getters
|
164
|
+
def horizontal_padding
|
165
|
+
config[:padding][:horizontal]
|
166
|
+
end
|
167
|
+
|
168
|
+
def vertical_padding
|
169
|
+
config[:padding][:vertical]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Positioning getters
|
173
|
+
def positioning(layer_type)
|
174
|
+
config[:positioning][layer_type] || {}
|
175
|
+
end
|
176
|
+
|
177
|
+
# Frame styling getters
|
178
|
+
def stroke_width
|
179
|
+
config[:frames][:stroke_width]
|
180
|
+
end
|
181
|
+
|
182
|
+
def corner_radius(layer_type)
|
183
|
+
config[:frames][:corner_radius][layer_type] || { x: 5, y: 5 }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Mana cost getters
|
187
|
+
def mana_cost_config
|
188
|
+
config[:mana_cost]
|
189
|
+
end
|
190
|
+
|
191
|
+
# Copyright getters
|
192
|
+
def copyright_config
|
193
|
+
config[:copyright]
|
194
|
+
end
|
195
|
+
|
196
|
+
# Type icon getters
|
197
|
+
def type_icon_config
|
198
|
+
config[:type_icon]
|
199
|
+
end
|
200
|
+
|
201
|
+
# Text rendering getters
|
202
|
+
def text_rendering_config
|
203
|
+
config[:text_rendering]
|
204
|
+
end
|
205
|
+
|
206
|
+
def default_font_size
|
207
|
+
text_rendering_config[:default_font_size]
|
208
|
+
end
|
209
|
+
|
210
|
+
def default_text_color
|
211
|
+
text_rendering_config[:default_color]
|
212
|
+
end
|
213
|
+
|
214
|
+
def default_line_height_multiplier
|
215
|
+
text_rendering_config[:default_line_height_multiplier]
|
216
|
+
end
|
217
|
+
|
218
|
+
def char_width_multiplier
|
219
|
+
text_rendering_config[:char_width_multiplier]
|
220
|
+
end
|
221
|
+
|
222
|
+
def css_class(layer_type)
|
223
|
+
text_rendering_config[:css_classes][layer_type]
|
224
|
+
end
|
225
|
+
|
226
|
+
# Frame getters
|
227
|
+
def frame_config
|
228
|
+
config[:frame]
|
229
|
+
end
|
230
|
+
|
231
|
+
def frame_bottom_margin
|
232
|
+
frame_config[:bottom_margin]
|
233
|
+
end
|
234
|
+
|
235
|
+
# Drop shadow getters
|
236
|
+
def drop_shadow_config
|
237
|
+
config[:drop_shadow]
|
238
|
+
end
|
239
|
+
|
240
|
+
# Text positioning getters
|
241
|
+
def text_positioning_config
|
242
|
+
config[:text_positioning]
|
243
|
+
end
|
244
|
+
|
245
|
+
def mana_cost_text_y_offset
|
246
|
+
text_positioning_config[:mana_cost_text_y_offset]
|
247
|
+
end
|
248
|
+
|
249
|
+
def mana_cost_font_size
|
250
|
+
text_positioning_config[:mana_cost_font_size]
|
251
|
+
end
|
252
|
+
|
253
|
+
# Icon opacity getters
|
254
|
+
def icon_opacity_config
|
255
|
+
config[:icon_opacity]
|
256
|
+
end
|
257
|
+
|
258
|
+
def mana_cost_icon_opacity
|
259
|
+
icon_opacity_config[:mana_cost]
|
260
|
+
end
|
261
|
+
|
262
|
+
# Convenience methods for common calculations
|
263
|
+
def text_width(base_width, layer_type = nil)
|
264
|
+
if layer_type && positioning(layer_type)[:width_ratio]
|
265
|
+
(base_width * positioning(layer_type)[:width_ratio]) - (horizontal_padding * 2)
|
266
|
+
else
|
267
|
+
base_width - (horizontal_padding * 2)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def text_x_position(base_x)
|
272
|
+
base_x + horizontal_padding
|
273
|
+
end
|
274
|
+
|
275
|
+
def text_y_position(base_y, layer_type, height = nil)
|
276
|
+
offset = positioning(layer_type)[:y_offset] || 0
|
277
|
+
if height
|
278
|
+
base_y + (height / 2) + offset
|
279
|
+
else
|
280
|
+
base_y + offset
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Class method for easy access to default config
|
285
|
+
def self.default
|
286
|
+
new
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'layers/art_layer'
|
4
|
+
require_relative 'layers/text_box_layer'
|
5
|
+
require_relative 'layers/frame_layer'
|
6
|
+
require_relative 'layers/name_layer'
|
7
|
+
require_relative 'layers/border_layer'
|
8
|
+
require_relative 'layers/power_layer'
|
9
|
+
require_relative 'layers/type_line_layer'
|
10
|
+
|
11
|
+
module MtgCardMaker
|
12
|
+
# Centralized factory for creating card layers that eliminates duplication
|
13
|
+
# between BaseCard and SpriteSheetService. This factory handles the creation
|
14
|
+
# of all layer types with proper configuration and dependencies.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# layers = MtgCardMaker::LayerFactory.create_layers_for_card(card, mask_id, card_config)
|
18
|
+
# layers.each { |layer| template.add_layer(layer) }
|
19
|
+
#
|
20
|
+
# @since 0.1.0
|
21
|
+
class LayerFactory
|
22
|
+
# Create all layers for a card using the factory pattern
|
23
|
+
#
|
24
|
+
# @param card [BaseCard] the card instance
|
25
|
+
# @param mask_id [String] the mask ID for art window
|
26
|
+
# @param card_config [Object] the card configuration object
|
27
|
+
# @return [Array<BaseLayer>] an array of all card layers
|
28
|
+
def self.create_layers_for_card(card, mask_id, card_config)
|
29
|
+
new(card, mask_id, card_config).create_layers
|
30
|
+
end
|
31
|
+
|
32
|
+
# Initialize a new layer factory
|
33
|
+
#
|
34
|
+
# @param card [BaseCard] the card instance
|
35
|
+
# @param mask_id [String] the mask ID for art window
|
36
|
+
# @param card_config [Object] the card configuration object
|
37
|
+
def initialize(card, mask_id, card_config)
|
38
|
+
@card = card
|
39
|
+
@mask_id = mask_id
|
40
|
+
@card_config = card_config
|
41
|
+
@color_scheme = card.color_scheme
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create all layers for the card in the correct order
|
45
|
+
#
|
46
|
+
# @return [Array<BaseLayer>] an array of all card layers in rendering order
|
47
|
+
def create_layers
|
48
|
+
[
|
49
|
+
create_border_layer,
|
50
|
+
create_frame_layer,
|
51
|
+
create_name_layer,
|
52
|
+
create_art_layer,
|
53
|
+
create_type_line_layer,
|
54
|
+
create_text_box_layer,
|
55
|
+
create_power_layer
|
56
|
+
]
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def create_border_layer
|
62
|
+
border_color = @card.border_color || :white
|
63
|
+
BorderLayer.new(
|
64
|
+
dimensions: @card_config.dimensions_for_layer(:border),
|
65
|
+
color: border_color,
|
66
|
+
mask_id: @mask_id
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_frame_layer
|
71
|
+
FrameLayer.new(
|
72
|
+
dimensions: @card_config.dimensions_for_layer(:frame),
|
73
|
+
color_scheme: @color_scheme,
|
74
|
+
mask_id: @mask_id
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_name_layer
|
79
|
+
NameLayer.new(
|
80
|
+
dimensions: @card_config.dimensions_for_layer(:name_area),
|
81
|
+
name: @card.name,
|
82
|
+
cost: @card.mana_cost,
|
83
|
+
color_scheme: @color_scheme
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_art_layer
|
88
|
+
art = @card.art
|
89
|
+
ArtLayer.new(
|
90
|
+
dimensions: @card_config.dimensions_for_layer(:art_layer),
|
91
|
+
color_scheme: @color_scheme,
|
92
|
+
art: art
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_type_line_layer
|
97
|
+
TypeLineLayer.new(
|
98
|
+
dimensions: @card_config.dimensions_for_layer(:type_area),
|
99
|
+
type_line: @card.type_line,
|
100
|
+
color_scheme: @color_scheme
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_text_box_layer
|
105
|
+
TextBoxLayer.new(
|
106
|
+
dimensions: @card_config.dimensions_for_layer(:text_box),
|
107
|
+
rules_text: @card.rules_text,
|
108
|
+
flavor_text: @card.flavor_text,
|
109
|
+
color_scheme: @color_scheme
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def create_power_layer
|
114
|
+
PowerLayer.new(
|
115
|
+
dimensions: @card_config.dimensions_for_layer(:power_area),
|
116
|
+
power: @card.power,
|
117
|
+
toughness: @card.toughness,
|
118
|
+
color_scheme: @color_scheme
|
119
|
+
)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# Mixin to provide common layer initialization patterns
|
5
|
+
module LayerInitializer
|
6
|
+
private
|
7
|
+
|
8
|
+
def initialize_layer_color(color, color_scheme, default_method)
|
9
|
+
color || color_scheme.send(default_method)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'addressable/uri'
|
4
|
+
require_relative '../../mtg_card_maker'
|
5
|
+
|
6
|
+
module MtgCardMaker
|
7
|
+
# ArtLayer is a specialized layer for card artwork.
|
8
|
+
# It provides a placeholder for the art area,
|
9
|
+
# the art itself is not a feature of this gem.
|
10
|
+
class ArtLayer < BaseLayer
|
11
|
+
attr_reader :color_scheme, :art
|
12
|
+
|
13
|
+
def initialize(dimensions:, color_scheme: DEFAULT_COLOR_SCHEME, art: nil)
|
14
|
+
super(dimensions: dimensions, color: '#000')
|
15
|
+
@color_scheme = color_scheme
|
16
|
+
@art = parse_image_url(art)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Render the art area with an ornate frame and optional image
|
20
|
+
def render
|
21
|
+
layer_config = LayerConfig.default
|
22
|
+
stroke_width = layer_config.stroke_width
|
23
|
+
outer_corners = layer_config.corner_radius(:art)
|
24
|
+
inner_corners = { x: 5, y: 5 } # Inner frame uses smaller radius
|
25
|
+
|
26
|
+
svg.g do
|
27
|
+
# Ornate art frame
|
28
|
+
svg.rect x: x - 3, y: y - 3,
|
29
|
+
width: width + 6, height: height + 6,
|
30
|
+
fill: 'none',
|
31
|
+
stroke: color_scheme.primary_color,
|
32
|
+
stroke_width: stroke_width,
|
33
|
+
rx: outer_corners[:x], ry: outer_corners[:y]
|
34
|
+
|
35
|
+
# Frame border for the transparent window
|
36
|
+
svg.rect x: x, y: y, width: width, height: height, fill: 'none',
|
37
|
+
stroke: ColorPalette::FRAME_STROKE_COLOR, stroke_width: stroke_width,
|
38
|
+
rx: inner_corners[:x], ry: inner_corners[:y]
|
39
|
+
|
40
|
+
# Render image if provided
|
41
|
+
render_image if art
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def parse_image_url(url)
|
48
|
+
return nil if url.nil? || url.empty?
|
49
|
+
|
50
|
+
begin
|
51
|
+
Addressable::URI.parse(url).to_s
|
52
|
+
rescue Addressable::URI::InvalidURIError => e
|
53
|
+
raise ArgumentError, "Invalid image URL: '#{url}'. Error: #{e.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_image
|
58
|
+
svg.image href: art,
|
59
|
+
x: x, y: y, width: width, height: height,
|
60
|
+
preserveAspectRatio: 'xMidYMid slice'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../metallic_renderer'
|
4
|
+
|
5
|
+
module MtgCardMaker
|
6
|
+
# BorderLayer is a specialized layer for the border of the card.
|
7
|
+
# It renders the outermost border of the card and supports four color options:
|
8
|
+
# white, black, silver (colorless), and gold. The layer includes QR codes
|
9
|
+
# and copyright text at the bottom.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# layer = MtgCardMaker::BorderLayer.new(
|
13
|
+
# dimensions: { x: 0, y: 0, width: 630, height: 880 },
|
14
|
+
# color: :gold,
|
15
|
+
# mask_id: 'artWindowMask'
|
16
|
+
# )
|
17
|
+
# layer.render
|
18
|
+
#
|
19
|
+
# @since 0.1.0
|
20
|
+
class BorderLayer < BaseLayer
|
21
|
+
include MetallicRenderer
|
22
|
+
include LayerInitializer
|
23
|
+
|
24
|
+
# @return [ColorScheme] the color scheme for this layer
|
25
|
+
attr_reader :color_scheme
|
26
|
+
|
27
|
+
# @return [String] the mask ID for the art window
|
28
|
+
attr_reader :mask_id
|
29
|
+
|
30
|
+
# Supported colors for the border
|
31
|
+
# @return [Hash] the supported color mappings
|
32
|
+
SUPPORTED_COLORS = {
|
33
|
+
white: '#EEE',
|
34
|
+
black: '#000',
|
35
|
+
silver: :colorless,
|
36
|
+
gold: :gold
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
# Initialize a new border layer
|
40
|
+
#
|
41
|
+
# @param dimensions [Hash] the layer dimensions
|
42
|
+
# @option dimensions [Integer] :x the x-coordinate
|
43
|
+
# @option dimensions [Integer] :y the y-coordinate
|
44
|
+
# @option dimensions [Integer] :width the width
|
45
|
+
# @option dimensions [Integer] :height the height
|
46
|
+
# @param color [Symbol] the frame color (default: :white)
|
47
|
+
# @param mask_id [String] the mask ID for art window (default: 'artWindowMask')
|
48
|
+
# @raise [ArgumentError] if the color is not supported
|
49
|
+
def initialize(dimensions:, color: :white, mask_id: 'artWindowMask')
|
50
|
+
@color = color.to_sym
|
51
|
+
validate_color!
|
52
|
+
|
53
|
+
@mask_id = mask_id
|
54
|
+
@icon_service = IconService.new
|
55
|
+
@color_scheme = ColorScheme.new(color_key_for(@color))
|
56
|
+
super(dimensions: dimensions, color: @color)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Render the border layer with QR code and copyright text
|
60
|
+
#
|
61
|
+
# @return [void]
|
62
|
+
def render
|
63
|
+
layer_config = LayerConfig.default
|
64
|
+
corners = layer_config.corner_radius(:outer)
|
65
|
+
color_key = color_key_for(@color)
|
66
|
+
|
67
|
+
render_frame_by_color(color_key, corners)
|
68
|
+
render_qr_code
|
69
|
+
render_copyright_texts(layer_config)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def render_frame_by_color(color, corners)
|
75
|
+
case color
|
76
|
+
when :white
|
77
|
+
render_white_frame(corners)
|
78
|
+
when :black
|
79
|
+
render_black_frame(corners)
|
80
|
+
when :colorless, :gold
|
81
|
+
render_metallic_frame(corners)
|
82
|
+
else
|
83
|
+
raise ArgumentError, "Unsupported border color: #{color.inspect}. Supported: white, black, silver, gold."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def render_white_frame(corners)
|
88
|
+
svg.rect x: x, y: y, width: width, height: height,
|
89
|
+
fill: SUPPORTED_COLORS[:white], rx: corners[:x], ry: corners[:y],
|
90
|
+
mask: "url(##{@mask_id})"
|
91
|
+
end
|
92
|
+
|
93
|
+
def render_black_frame(corners)
|
94
|
+
svg.rect x: x, y: y, width: width, height: height,
|
95
|
+
fill: SUPPORTED_COLORS[:black], rx: corners[:x], ry: corners[:y],
|
96
|
+
mask: "url(##{@mask_id})"
|
97
|
+
end
|
98
|
+
|
99
|
+
def render_metallic_frame(corners)
|
100
|
+
svg.rect x: x, y: y,
|
101
|
+
width: width, height: height,
|
102
|
+
rx: corners[:x], ry: corners[:y],
|
103
|
+
fill: '#EEE',
|
104
|
+
mask: "url(##{@mask_id})"
|
105
|
+
SvgGradientService.define_all_gradients(svg, color_scheme)
|
106
|
+
render_metallic_elements(
|
107
|
+
mask: @mask_id,
|
108
|
+
corners: corners,
|
109
|
+
geometry: { x: x, y: y, width: width, height: height, padding: 0 },
|
110
|
+
bottom_margin: 0,
|
111
|
+
opacity: { texture: 0.15, shadow: 0.18 }
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate_color!
|
116
|
+
return if SUPPORTED_COLORS.key?(@color)
|
117
|
+
|
118
|
+
raise ArgumentError, "Unsupported border color: #{@color.inspect}. Supported: white, black, silver, gold."
|
119
|
+
end
|
120
|
+
|
121
|
+
def color_key_for(color)
|
122
|
+
return :colorless if color == :silver
|
123
|
+
return :gold if color == :gold
|
124
|
+
|
125
|
+
color
|
126
|
+
end
|
127
|
+
|
128
|
+
def fill_color
|
129
|
+
@color == :black ? '#FFF' : '#111'
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_qr_code
|
133
|
+
qr_svg_content = @icon_service.qr_code_svg
|
134
|
+
raise 'QR code SVG content is nil' unless qr_svg_content
|
135
|
+
|
136
|
+
# Extract the path data from the SVG content
|
137
|
+
path_match = qr_svg_content.match(/<path[^>]*d="([^"]*)"[^>]*>/)
|
138
|
+
raise 'No path element found in QR code SVG' unless path_match
|
139
|
+
|
140
|
+
svg.path fill: fill_color, transform: 'translate(40,820) scale(1.1)',
|
141
|
+
d: path_match[1]
|
142
|
+
end
|
143
|
+
|
144
|
+
def render_copyright_texts(layer_config)
|
145
|
+
copyright_texts = [
|
146
|
+
'© 2025 Joe Sharp. Some rights reserved.',
|
147
|
+
'Portions of the materials used are property of Wizards of the Coast.',
|
148
|
+
'© Wizards of the Coast LLC'
|
149
|
+
]
|
150
|
+
|
151
|
+
copyright_config = layer_config.copyright_config
|
152
|
+
|
153
|
+
copyright_texts.each_with_index do |text, index|
|
154
|
+
copyright_attrs = {
|
155
|
+
x: copyright_config[:x_position],
|
156
|
+
y: copyright_config[:base_y] + (index * copyright_config[:line_spacing]),
|
157
|
+
fill: fill_color,
|
158
|
+
font_size: layer_config.font_size(:copyright),
|
159
|
+
text_anchor: 'start',
|
160
|
+
class: 'card-copyright'
|
161
|
+
}
|
162
|
+
svg.text text, copyright_attrs
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../metallic_renderer'
|
4
|
+
|
5
|
+
module MtgCardMaker
|
6
|
+
# FrameLayer is a specialized layer for the colored frame of the card
|
7
|
+
# You choose the color, making it possible to make a blue card
|
8
|
+
# that costs mountains, for example.
|
9
|
+
class FrameLayer < BaseLayer
|
10
|
+
include MetallicRenderer
|
11
|
+
include LayerInitializer
|
12
|
+
attr_reader :color_scheme, :mask_id, :layer_config
|
13
|
+
|
14
|
+
def initialize(dimensions:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME, mask_id: 'artWindowMask')
|
15
|
+
frame_color = initialize_layer_color(color, color_scheme, :primary_color)
|
16
|
+
super(dimensions: dimensions, color: frame_color)
|
17
|
+
@color_scheme = color_scheme
|
18
|
+
@mask_id = mask_id
|
19
|
+
@layer_config = LayerConfig.default
|
20
|
+
end
|
21
|
+
|
22
|
+
# Render the frame with a gradient background
|
23
|
+
def render
|
24
|
+
SvgGradientService.define_all_gradients(svg, color_scheme)
|
25
|
+
if metallic_properties?
|
26
|
+
render_metallic_frame
|
27
|
+
else
|
28
|
+
render_standard_frame
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def render_metallic_frame
|
35
|
+
render_standard_frame
|
36
|
+
render_metallic_elements(
|
37
|
+
mask: @mask_id,
|
38
|
+
bottom_margin: layer_config.frame_bottom_margin,
|
39
|
+
opacity: { texture: 0.3, shadow: 0.4 }
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def metallic_properties?
|
44
|
+
color_scheme.scheme_name == :gold
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_standard_frame # rubocop:disable Metrics/AbcSize
|
48
|
+
stroke_width = layer_config.stroke_width
|
49
|
+
corners = layer_config.corner_radius(:inner)
|
50
|
+
padding = layer_config.horizontal_padding
|
51
|
+
bottom_margin = layer_config.frame_bottom_margin # Space for type line and power/toughness
|
52
|
+
|
53
|
+
# Inner decorative frame with mask for transparent window
|
54
|
+
svg.rect x: x + padding, y: y + padding,
|
55
|
+
width: width - (padding * 2), height: height - bottom_margin,
|
56
|
+
fill: "url(##{SvgGradientService.frame_gradient_id(color_scheme)})",
|
57
|
+
stroke: color_scheme.primary_color,
|
58
|
+
stroke_width: stroke_width,
|
59
|
+
rx: corners[:x], ry: corners[:y], mask: "url(##{@mask_id})"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|