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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/CODE_OF_CONDUCT.md +131 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.md +305 -0
  6. data/bin/mtg_card_maker +7 -0
  7. data/lib/mtg_card_maker/base_card.rb +210 -0
  8. data/lib/mtg_card_maker/cli.rb +221 -0
  9. data/lib/mtg_card_maker/color_palette.rb +135 -0
  10. data/lib/mtg_card_maker/color_scheme.rb +305 -0
  11. data/lib/mtg_card_maker/core_ext/deep_merge.rb +21 -0
  12. data/lib/mtg_card_maker/fonts/Goudy Mediaeval DemiBold.ttf +0 -0
  13. data/lib/mtg_card_maker/fonts/goudy_base64.txt +1 -0
  14. data/lib/mtg_card_maker/icon_service.rb +95 -0
  15. data/lib/mtg_card_maker/icons/black.svg +1 -0
  16. data/lib/mtg_card_maker/icons/blue.svg +1 -0
  17. data/lib/mtg_card_maker/icons/colorless.svg +1 -0
  18. data/lib/mtg_card_maker/icons/green.svg +1 -0
  19. data/lib/mtg_card_maker/icons/jsharp.svg +1 -0
  20. data/lib/mtg_card_maker/icons/qrcode.svg +1 -0
  21. data/lib/mtg_card_maker/icons/red.svg +1 -0
  22. data/lib/mtg_card_maker/icons/white.svg +1 -0
  23. data/lib/mtg_card_maker/layer_config.rb +289 -0
  24. data/lib/mtg_card_maker/layer_factory.rb +122 -0
  25. data/lib/mtg_card_maker/layer_initializer.rb +12 -0
  26. data/lib/mtg_card_maker/layers/art_layer.rb +63 -0
  27. data/lib/mtg_card_maker/layers/border_layer.rb +166 -0
  28. data/lib/mtg_card_maker/layers/frame_layer.rb +62 -0
  29. data/lib/mtg_card_maker/layers/name_layer.rb +82 -0
  30. data/lib/mtg_card_maker/layers/power_layer.rb +69 -0
  31. data/lib/mtg_card_maker/layers/text_box_layer.rb +107 -0
  32. data/lib/mtg_card_maker/layers/type_line_layer.rb +86 -0
  33. data/lib/mtg_card_maker/mana_cost.rb +220 -0
  34. data/lib/mtg_card_maker/metallic_renderer.rb +174 -0
  35. data/lib/mtg_card_maker/sprite_sheet_assets.rb +158 -0
  36. data/lib/mtg_card_maker/sprite_sheet_builder.rb +90 -0
  37. data/lib/mtg_card_maker/sprite_sheet_service.rb +126 -0
  38. data/lib/mtg_card_maker/svg_gradient_service.rb +159 -0
  39. data/lib/mtg_card_maker/text_rendering_service.rb +160 -0
  40. data/lib/mtg_card_maker/version.rb +8 -0
  41. data/lib/mtg_card_maker.rb +268 -0
  42. data/sig/mtg_card_maker.rbs +4 -0
  43. 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