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,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# Simplified gradient definitions for SVG cards
|
5
|
+
# Removes complex abstraction in favor of direct gradient creation
|
6
|
+
class SvgGradientService # rubocop:disable Metrics/ClassLength
|
7
|
+
class << self
|
8
|
+
# Defines all gradients needed for a color scheme
|
9
|
+
def define_all_gradients(svg, color_scheme = DEFAULT_COLOR_SCHEME)
|
10
|
+
svg.defs do
|
11
|
+
define_standard_gradients(svg, color_scheme)
|
12
|
+
define_metallic_gradients(svg, color_scheme) if metallic_properties?(color_scheme)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get gradient ID for card gradient
|
17
|
+
def card_gradient_id(color_scheme)
|
18
|
+
"#{color_scheme.scheme_name}_card_gradient"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get gradient ID for frame gradient
|
22
|
+
def frame_gradient_id(color_scheme)
|
23
|
+
"#{color_scheme.scheme_name}_frame_gradient"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get gradient ID for name gradient
|
27
|
+
def name_gradient_id(color_scheme)
|
28
|
+
"#{color_scheme.scheme_name}_name_gradient"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get gradient ID for description gradient
|
32
|
+
def description_gradient_id(color_scheme)
|
33
|
+
"#{color_scheme.scheme_name}_description_gradient"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get gradient ID for metallic highlight gradient
|
37
|
+
def metallic_highlight_gradient_id(color_scheme)
|
38
|
+
"#{color_scheme.scheme_name}_metallic_highlight_gradient"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get gradient ID for metallic shadow gradient
|
42
|
+
def metallic_shadow_gradient_id(color_scheme)
|
43
|
+
"#{color_scheme.scheme_name}_metallic_shadow_gradient"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get pattern ID for metallic texture
|
47
|
+
def metallic_pattern_id(color_scheme)
|
48
|
+
"#{color_scheme.scheme_name}_metallic_pattern"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check if color scheme has metallic properties
|
52
|
+
def metallic_properties?(color_scheme)
|
53
|
+
[:gold, :colorless].include?(color_scheme.scheme_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
def define_standard_gradients(svg, color_scheme)
|
57
|
+
scheme_name = color_scheme.scheme_name
|
58
|
+
|
59
|
+
define_card_gradient(svg, color_scheme, scheme_name)
|
60
|
+
define_frame_gradient(svg, color_scheme, scheme_name)
|
61
|
+
define_name_gradient(svg, color_scheme, scheme_name)
|
62
|
+
define_description_gradient(svg, color_scheme, scheme_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
def define_card_gradient(svg, color_scheme, scheme_name)
|
66
|
+
svg.linearGradient id: "#{scheme_name}_card_gradient", x1: '0%', y1: '0%', x2: '100%', y2: '100%' do
|
67
|
+
svg.stop offset: '0%', 'stop-color': color_scheme.card_gradient_start
|
68
|
+
svg.stop offset: '50%', 'stop-color': color_scheme.card_gradient_middle
|
69
|
+
svg.stop offset: '100%', 'stop-color': color_scheme.card_gradient_end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def define_frame_gradient(svg, color_scheme, scheme_name)
|
74
|
+
svg.linearGradient id: "#{scheme_name}_frame_gradient", x1: '0%', y1: '0%', x2: '100%', y2: '100%' do
|
75
|
+
svg.stop offset: '0%', 'stop-color': color_scheme.frame_gradient_start
|
76
|
+
svg.stop offset: '50%', 'stop-color': color_scheme.frame_gradient_middle
|
77
|
+
svg.stop offset: '100%', 'stop-color': color_scheme.frame_gradient_end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def define_name_gradient(svg, color_scheme, scheme_name)
|
82
|
+
svg.linearGradient id: "#{scheme_name}_name_gradient", x1: '0%', y1: '0%', x2: '100%', y2: '100%' do
|
83
|
+
svg.stop offset: '0%', 'stop-color': color_scheme.name_gradient_start
|
84
|
+
svg.stop offset: '50%', 'stop-color': color_scheme.name_gradient_middle
|
85
|
+
svg.stop offset: '100%', 'stop-color': color_scheme.name_gradient_end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def define_description_gradient(svg, color_scheme, scheme_name)
|
90
|
+
svg.linearGradient id: "#{scheme_name}_description_gradient", x1: '0%', y1: '0%', x2: '100%', y2: '100%' do
|
91
|
+
svg.stop offset: '0%', 'stop-color': color_scheme.description_gradient_start
|
92
|
+
svg.stop offset: '50%', 'stop-color': color_scheme.description_gradient_middle
|
93
|
+
svg.stop offset: '100%', 'stop-color': color_scheme.description_gradient_end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def define_metallic_gradients(svg, color_scheme)
|
98
|
+
scheme_name = color_scheme.scheme_name
|
99
|
+
|
100
|
+
define_metallic_highlight_gradient(svg, color_scheme, scheme_name)
|
101
|
+
define_metallic_shadow_gradient(svg, color_scheme, scheme_name)
|
102
|
+
define_metallic_pattern(svg, color_scheme, scheme_name)
|
103
|
+
end
|
104
|
+
|
105
|
+
def define_metallic_highlight_gradient(svg, color_scheme, scheme_name)
|
106
|
+
svg.linearGradient id: "#{scheme_name}_metallic_highlight_gradient", x1: '0%', y1: '0%', x2: '100%',
|
107
|
+
y2: '100%' do
|
108
|
+
svg.stop offset: '0%', 'stop-color': color_scheme.metallic_highlight_end, 'stop-opacity': '0.6'
|
109
|
+
svg.stop offset: '20%', 'stop-color': color_scheme.metallic_highlight_middle, 'stop-opacity': '0.9'
|
110
|
+
svg.stop offset: '38%', 'stop-color': color_scheme.metallic_highlight_start, 'stop-opacity': '1.0'
|
111
|
+
svg.stop offset: '42%', 'stop-color': color_scheme.metallic_highlight_middle, 'stop-opacity': '0.7'
|
112
|
+
svg.stop offset: '62%', 'stop-color': color_scheme.metallic_highlight_end, 'stop-opacity': '0.5'
|
113
|
+
svg.stop offset: '73%', 'stop-color': color_scheme.metallic_highlight_start, 'stop-opacity': '0.8'
|
114
|
+
svg.stop offset: '80%', 'stop-color': color_scheme.metallic_highlight_middle, 'stop-opacity': '0.6'
|
115
|
+
svg.stop offset: '100%', 'stop-color': color_scheme.metallic_highlight_end, 'stop-opacity': '0.4'
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def define_metallic_shadow_gradient(svg, color_scheme, scheme_name)
|
120
|
+
svg.radialGradient id: "#{scheme_name}_metallic_shadow_gradient", cx: '50%', cy: '50%', r: '70%' do
|
121
|
+
svg.stop offset: '0%', 'stop-color': color_scheme.metallic_shadow_start, 'stop-opacity': '0.3'
|
122
|
+
svg.stop offset: '50%', 'stop-color': color_scheme.metallic_shadow_middle, 'stop-opacity': '0.5'
|
123
|
+
svg.stop offset: '100%', 'stop-color': color_scheme.metallic_shadow_end, 'stop-opacity': '0.7'
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def define_metallic_pattern(svg, color_scheme, scheme_name)
|
128
|
+
svg.pattern id: "#{scheme_name}_metallic_pattern", x: '0', y: '0', width: '20', height: '20',
|
129
|
+
patternUnits: 'userSpaceOnUse' do
|
130
|
+
add_metallic_pattern_lines(svg, color_scheme)
|
131
|
+
add_metallic_pattern_circles(svg, color_scheme)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def add_metallic_pattern_lines(svg, color_scheme)
|
136
|
+
svg.line x1: '0', y1: '0', x2: '20', y2: '20',
|
137
|
+
stroke: color_scheme.metallic_pattern_light,
|
138
|
+
'stroke-width': '0.5',
|
139
|
+
opacity: '0.3'
|
140
|
+
svg.line x1: '20', y1: '0', x2: '0', y2: '20',
|
141
|
+
stroke: color_scheme.metallic_pattern_dark,
|
142
|
+
'stroke-width': '0.5',
|
143
|
+
opacity: '0.2'
|
144
|
+
end
|
145
|
+
|
146
|
+
def add_metallic_pattern_circles(svg, color_scheme)
|
147
|
+
svg.circle cx: '5', cy: '5', r: '0.5',
|
148
|
+
fill: color_scheme.metallic_pattern_light,
|
149
|
+
opacity: '0.6'
|
150
|
+
svg.circle cx: '15', cy: '15', r: '0.5',
|
151
|
+
fill: color_scheme.metallic_pattern_light,
|
152
|
+
opacity: '0.6'
|
153
|
+
svg.circle cx: '10', cy: '10', r: '0.3',
|
154
|
+
fill: color_scheme.metallic_pattern_dark,
|
155
|
+
opacity: '0.4'
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MtgCardMaker
|
4
|
+
# Minimal text rendering service with basic word wrapping for SVG.
|
5
|
+
# This service handles text layout, wrapping, and attribute generation
|
6
|
+
# for SVG text elements with configurable font sizes, colors, and spacing.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# service = MtgCardMaker::TextRenderingService.new(
|
10
|
+
# text: "Lightning Bolt deals 3 damage to any target.",
|
11
|
+
# x: 40, y: 545, font_size: 24, available_width: 550
|
12
|
+
# )
|
13
|
+
# lines = service.wrapped_text_lines
|
14
|
+
#
|
15
|
+
# @since 0.1.0
|
16
|
+
class TextRenderingService
|
17
|
+
# @return [String] the text to render
|
18
|
+
attr_accessor :text
|
19
|
+
|
20
|
+
# @return [LayerConfig] the layer configuration
|
21
|
+
attr_accessor :layer_config
|
22
|
+
|
23
|
+
# @return [Integer] the x-coordinate for text positioning
|
24
|
+
attr_accessor :x
|
25
|
+
|
26
|
+
# @return [Integer] the y-coordinate for text positioning
|
27
|
+
attr_accessor :y
|
28
|
+
|
29
|
+
# @return [Integer] the font size
|
30
|
+
attr_accessor :font_size
|
31
|
+
|
32
|
+
# @return [String] the text color
|
33
|
+
attr_accessor :color
|
34
|
+
|
35
|
+
# @return [Integer] the available width for text wrapping
|
36
|
+
attr_accessor :available_width
|
37
|
+
|
38
|
+
# @return [Integer] the line height
|
39
|
+
attr_accessor :line_height
|
40
|
+
|
41
|
+
# @return [String, nil] the CSS class for styling
|
42
|
+
attr_accessor :css_class
|
43
|
+
|
44
|
+
# Initialize a new text rendering service
|
45
|
+
#
|
46
|
+
# @param kwargs [Hash] the initialization parameters
|
47
|
+
# @option kwargs [String] :text the text to render (default: '')
|
48
|
+
# @option kwargs [LayerConfig] :layer_config the layer configuration (default: LayerConfig.default)
|
49
|
+
# @option kwargs [Integer] :x the x-coordinate (default: 0)
|
50
|
+
# @option kwargs [Integer] :y the y-coordinate (default: 0)
|
51
|
+
# @option kwargs [Integer] :font_size the font size (default: from layer_config)
|
52
|
+
# @option kwargs [String] :color the text color (default: from layer_config)
|
53
|
+
# @option kwargs [Integer] :available_width the available width (default: CARD_WIDTH)
|
54
|
+
# @option kwargs [Integer] :line_height the line height (default: calculated)
|
55
|
+
# @option kwargs [String] :css_class the CSS class (default: nil)
|
56
|
+
def initialize(**kwargs)
|
57
|
+
# Set defaults
|
58
|
+
defaults = {
|
59
|
+
text: '',
|
60
|
+
layer_config: LayerConfig.default,
|
61
|
+
x: 0,
|
62
|
+
y: 0,
|
63
|
+
font_size: nil,
|
64
|
+
color: nil,
|
65
|
+
available_width: nil,
|
66
|
+
line_height: nil,
|
67
|
+
css_class: nil
|
68
|
+
}
|
69
|
+
|
70
|
+
# Merge defaults with provided kwargs
|
71
|
+
final_params = defaults.merge(kwargs)
|
72
|
+
|
73
|
+
# Set instance variables
|
74
|
+
final_params.each do |key, value|
|
75
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Post-process dependent values
|
79
|
+
@font_size ||= @layer_config.default_font_size
|
80
|
+
@color ||= @layer_config.default_text_color
|
81
|
+
@available_width ||= CARD_WIDTH
|
82
|
+
@line_height ||= calculate_default_line_height
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns an array of [line, attrs] for SVG text elements
|
86
|
+
#
|
87
|
+
# @param text [String, nil] optional text to override current text
|
88
|
+
# @return [Array<Array>] array of [line_text, attributes_hash] pairs
|
89
|
+
def wrapped_text_lines(text = nil)
|
90
|
+
@text = text if text
|
91
|
+
render_text_lines
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def calculate_default_line_height
|
97
|
+
@font_size * @layer_config.default_line_height_multiplier
|
98
|
+
end
|
99
|
+
|
100
|
+
def render_text_lines
|
101
|
+
lines = text_to_lines
|
102
|
+
build_text_attributes(lines)
|
103
|
+
end
|
104
|
+
|
105
|
+
def text_to_lines
|
106
|
+
@text.to_s.split(/\r?\n/).flat_map do |line|
|
107
|
+
wrap_line(line)
|
108
|
+
end.reject(&:empty?)
|
109
|
+
end
|
110
|
+
|
111
|
+
def build_text_attributes(lines)
|
112
|
+
lines.each_with_index.map do |line, idx|
|
113
|
+
attrs = {
|
114
|
+
x: @x,
|
115
|
+
y: @y + (idx * @line_height).to_i,
|
116
|
+
fill: @color,
|
117
|
+
font_size: @font_size
|
118
|
+
}
|
119
|
+
attrs[:class] = @css_class if @css_class
|
120
|
+
[line, attrs]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def wrap_line(line)
|
125
|
+
return [''] if line.strip.empty?
|
126
|
+
|
127
|
+
char_width = @font_size * @layer_config.char_width_multiplier
|
128
|
+
words = line.split(/\s+/)
|
129
|
+
process_words(words, char_width)
|
130
|
+
end
|
131
|
+
|
132
|
+
def process_words(words, char_width)
|
133
|
+
lines = []
|
134
|
+
current = ''
|
135
|
+
|
136
|
+
words.each do |word|
|
137
|
+
current, completed_line = process_word(word, current, char_width)
|
138
|
+
lines << completed_line if completed_line
|
139
|
+
end
|
140
|
+
lines << current unless current.empty?
|
141
|
+
lines
|
142
|
+
end
|
143
|
+
|
144
|
+
def process_word(word, current, char_width)
|
145
|
+
test = current.empty? ? word : "#{current} #{word}"
|
146
|
+
if test.length * char_width <= @available_width
|
147
|
+
[test, nil]
|
148
|
+
else
|
149
|
+
[word, current]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class << self
|
154
|
+
# Convenience method for backward compatibility
|
155
|
+
def wrapped_text_lines(*, **)
|
156
|
+
new(*, **).wrapped_text_lines
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'mtg_card_maker/version'
|
4
|
+
require 'victor'
|
5
|
+
|
6
|
+
# MTG Card Maker is a Ruby gem for creating Magic: The Gathering card templates.
|
7
|
+
# It uses the Victor SVG library to generate layered card designs with customizable
|
8
|
+
# colors, text, and artwork. The library provides a modular layer system where
|
9
|
+
# each component (frame, art, text areas) inherits from BaseLayer and implements
|
10
|
+
# its own rendering logic.
|
11
|
+
#
|
12
|
+
# @example Basic Usage
|
13
|
+
# card = MtgCardMaker::BaseCard.new(
|
14
|
+
# name: "Lightning Bolt",
|
15
|
+
# mana_cost: "R",
|
16
|
+
# type: "Instant",
|
17
|
+
# description: "Lightning Bolt deals 3 damage to any target.",
|
18
|
+
# color_scheme: :red
|
19
|
+
# )
|
20
|
+
# card.generate("lightning_bolt.svg")
|
21
|
+
#
|
22
|
+
# @example Custom Template
|
23
|
+
# template = MtgCardMaker::Template.new
|
24
|
+
# template.add_layer(MtgCardMaker::BorderLayer.new(dimensions: {...}, color: :red))
|
25
|
+
# template.add_layer(MtgCardMaker::NameLayer.new(dimensions: {...}, text: "Lightning Bolt"))
|
26
|
+
# template.save("custom_card.svg")
|
27
|
+
#
|
28
|
+
# @since 0.1.0
|
29
|
+
# @author Joe Sharp
|
30
|
+
module MtgCardMaker
|
31
|
+
# Standard MTG card width in pixels
|
32
|
+
# @return [Integer] the width of a standard MTG card
|
33
|
+
CARD_WIDTH = 630
|
34
|
+
|
35
|
+
# Standard MTG card height in pixels
|
36
|
+
# @return [Integer] the height of a standard MTG card
|
37
|
+
CARD_HEIGHT = 880
|
38
|
+
|
39
|
+
# Load unified color scheme system
|
40
|
+
require_relative 'mtg_card_maker/color_scheme'
|
41
|
+
require_relative 'mtg_card_maker/color_palette'
|
42
|
+
require_relative 'mtg_card_maker/layer_config'
|
43
|
+
|
44
|
+
# Default color scheme using colorless colors
|
45
|
+
# @return [ColorScheme] the default colorless color scheme
|
46
|
+
DEFAULT_COLOR_SCHEME = ColorScheme.new(:colorless)
|
47
|
+
|
48
|
+
# Default text color from the default color scheme
|
49
|
+
# @return [String] the default text color hex value
|
50
|
+
DEFAULT_TEXT_COLOR = DEFAULT_COLOR_SCHEME.text_color
|
51
|
+
|
52
|
+
# White color constant for border compatibility
|
53
|
+
# @return [String] the white color hex value
|
54
|
+
WHITE = '#EEE'
|
55
|
+
|
56
|
+
# Custom error class for MTG Card Maker specific errors
|
57
|
+
class Error < StandardError; end
|
58
|
+
|
59
|
+
# Base class for all card layers that provides common attributes and rendering interface.
|
60
|
+
# All layer classes (frames, text areas, art) inherit from this base class and
|
61
|
+
# implement their own specific rendering logic.
|
62
|
+
#
|
63
|
+
# @abstract Subclasses must implement the {#render} method
|
64
|
+
# @since 0.1.0
|
65
|
+
class BaseLayer
|
66
|
+
# @return [Template, nil] the template instance this layer belongs to
|
67
|
+
attr_accessor :template
|
68
|
+
|
69
|
+
# @return [Integer] the x-coordinate of the layer
|
70
|
+
attr_reader :x
|
71
|
+
|
72
|
+
# @return [Integer] the y-coordinate of the layer
|
73
|
+
attr_reader :y
|
74
|
+
|
75
|
+
# @return [Integer] the width of the layer
|
76
|
+
attr_reader :width
|
77
|
+
|
78
|
+
# @return [Integer] the height of the layer
|
79
|
+
attr_reader :height
|
80
|
+
|
81
|
+
# @return [String] the color of the layer
|
82
|
+
attr_reader :color
|
83
|
+
|
84
|
+
# Initialize a new layer with the given dimensions and color
|
85
|
+
#
|
86
|
+
# @param dimensions [Hash] the layer dimensions
|
87
|
+
# @option dimensions [Integer] :x the x-coordinate
|
88
|
+
# @option dimensions [Integer] :y the y-coordinate
|
89
|
+
# @option dimensions [Integer] :width the width
|
90
|
+
# @option dimensions [Integer] :height the height
|
91
|
+
# @param color [String] the color of the layer (default: 'white')
|
92
|
+
def initialize(dimensions:, color: 'white')
|
93
|
+
@x = dimensions[:x]
|
94
|
+
@y = dimensions[:y]
|
95
|
+
@width = dimensions[:width]
|
96
|
+
@height = dimensions[:height]
|
97
|
+
@color = color
|
98
|
+
@template = nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# Render the layer to the SVG canvas
|
102
|
+
#
|
103
|
+
# @abstract Subclasses must implement this method
|
104
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
105
|
+
def render
|
106
|
+
raise NotImplementedError, "Subclasses must implement the 'render' method."
|
107
|
+
end
|
108
|
+
|
109
|
+
# Access the SVG canvas through the template
|
110
|
+
#
|
111
|
+
# @return [Victor::SVG, nil] the SVG canvas instance
|
112
|
+
def svg
|
113
|
+
@template&.instance_variable_get(:@svg)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
require_relative 'mtg_card_maker/layer_initializer'
|
118
|
+
require_relative 'mtg_card_maker/base_card'
|
119
|
+
require_relative 'mtg_card_maker/layer_factory'
|
120
|
+
require_relative 'mtg_card_maker/svg_gradient_service'
|
121
|
+
require_relative 'mtg_card_maker/text_rendering_service'
|
122
|
+
require_relative 'mtg_card_maker/sprite_sheet_assets'
|
123
|
+
require_relative 'mtg_card_maker/sprite_sheet_builder'
|
124
|
+
require_relative 'mtg_card_maker/sprite_sheet_service'
|
125
|
+
require_relative 'mtg_card_maker/layers/border_layer'
|
126
|
+
require_relative 'mtg_card_maker/layers/frame_layer'
|
127
|
+
require_relative 'mtg_card_maker/layers/name_layer'
|
128
|
+
require_relative 'mtg_card_maker/layers/art_layer'
|
129
|
+
require_relative 'mtg_card_maker/layers/type_line_layer'
|
130
|
+
require_relative 'mtg_card_maker/layers/text_box_layer'
|
131
|
+
require_relative 'mtg_card_maker/layers/power_layer'
|
132
|
+
require_relative 'mtg_card_maker/cli'
|
133
|
+
|
134
|
+
# Main template class that manages the SVG canvas and layer composition.
|
135
|
+
# This class handles creating the SVG canvas, adding layers, and saving
|
136
|
+
# the final SVG file. It also manages font embedding and CSS styling.
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# template = MtgCardMaker::Template.new
|
140
|
+
# template.add_layer(MtgCardMaker::BorderLayer.new(dimensions: {...}))
|
141
|
+
# template.add_layer(MtgCardMaker::NameLayer.new(dimensions: {...}, text: "Lightning Bolt"))
|
142
|
+
# template.save("card.svg")
|
143
|
+
#
|
144
|
+
# @since 0.1.0
|
145
|
+
class Template
|
146
|
+
# @return [Integer] the width of the SVG canvas
|
147
|
+
attr_reader :width
|
148
|
+
|
149
|
+
# @return [Integer] the height of the SVG canvas
|
150
|
+
attr_reader :height
|
151
|
+
|
152
|
+
# Initialize a new template with the given dimensions
|
153
|
+
#
|
154
|
+
# @param width [Integer] the width of the canvas (default: CARD_WIDTH)
|
155
|
+
# @param height [Integer] the height of the canvas (default: CARD_HEIGHT)
|
156
|
+
# @param embed_font [Boolean] whether to embed the font as base64 (default: false)
|
157
|
+
def initialize(width: CARD_WIDTH, height: CARD_HEIGHT, embed_font: false)
|
158
|
+
@width = width
|
159
|
+
@height = height
|
160
|
+
@svg = Victor::SVG.new width: width, height: height
|
161
|
+
embed_font(embed: embed_font)
|
162
|
+
define_css_classes
|
163
|
+
end
|
164
|
+
|
165
|
+
# Add a layer to the template and render it
|
166
|
+
#
|
167
|
+
# @param layer [BaseLayer] the layer to add and render
|
168
|
+
# @return [void]
|
169
|
+
def add_layer(layer)
|
170
|
+
layer.template = self
|
171
|
+
layer.render
|
172
|
+
end
|
173
|
+
|
174
|
+
# Save the SVG to a file
|
175
|
+
#
|
176
|
+
# @param filename [String] the filename to save to
|
177
|
+
# @return [void]
|
178
|
+
def save(filename)
|
179
|
+
@svg.save(filename)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Get the SVG as a string
|
183
|
+
#
|
184
|
+
# @return [String] the SVG content as a string
|
185
|
+
def to_svg
|
186
|
+
@svg.to_s
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def embed_font(embed: false)
|
192
|
+
if embed
|
193
|
+
font_path = File.join(__dir__, 'mtg_card_maker', 'fonts', 'goudy_base64.txt')
|
194
|
+
base64_font_data = File.read(font_path).strip
|
195
|
+
@svg.style <<~CSS
|
196
|
+
@font-face {
|
197
|
+
font-family: 'Goudy Mediaeval DemiBold';
|
198
|
+
src: url(data:font/truetype;charset=utf-8;base64,#{base64_font_data}) format('truetype');
|
199
|
+
font-weight: normal;
|
200
|
+
font-style: normal;
|
201
|
+
}
|
202
|
+
CSS
|
203
|
+
else
|
204
|
+
@svg.style <<~CSS
|
205
|
+
@font-face {
|
206
|
+
font-family: 'Goudy Mediaeval DemiBold';
|
207
|
+
src: url('fonts/Goudy Mediaeval DemiBold.ttf') format('truetype');
|
208
|
+
font-weight: normal;
|
209
|
+
font-style: normal;
|
210
|
+
}
|
211
|
+
CSS
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def define_css_classes
|
216
|
+
@svg.style <<~CSS
|
217
|
+
/* Font Classes */
|
218
|
+
.card-name {
|
219
|
+
font-family: 'Goudy Mediaeval DemiBold', serif;
|
220
|
+
font-weight: normal;
|
221
|
+
font-style: normal;
|
222
|
+
}
|
223
|
+
|
224
|
+
.card-type {
|
225
|
+
font-family: 'Goudy Mediaeval DemiBold', serif;
|
226
|
+
font-weight: normal;
|
227
|
+
font-style: normal;
|
228
|
+
}
|
229
|
+
|
230
|
+
.card-description {
|
231
|
+
font-family: serif;
|
232
|
+
font-weight: normal;
|
233
|
+
font-style: normal;
|
234
|
+
}
|
235
|
+
|
236
|
+
.card-flavor-text {
|
237
|
+
font-family: serif;
|
238
|
+
font-weight: normal;
|
239
|
+
font-style: italic;
|
240
|
+
}
|
241
|
+
|
242
|
+
.card-power-toughness {
|
243
|
+
font-family: serif;
|
244
|
+
font-weight: bold;
|
245
|
+
font-style: normal;
|
246
|
+
}
|
247
|
+
|
248
|
+
.card-copyright {
|
249
|
+
font-family: sans-serif;
|
250
|
+
font-weight: normal;
|
251
|
+
font-style: normal;
|
252
|
+
}
|
253
|
+
|
254
|
+
.mana-cost-text {
|
255
|
+
font-family: serif;
|
256
|
+
font-weight: normal;
|
257
|
+
font-style: normal;
|
258
|
+
}
|
259
|
+
|
260
|
+
.mana-cost-text-large {
|
261
|
+
font-family: serif;
|
262
|
+
font-weight: semibold;
|
263
|
+
font-style: normal;
|
264
|
+
}
|
265
|
+
CSS
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|