mtg_card_maker 0.1.0 → 0.2.1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/LICENSE.txt +59 -14
  4. data/README.md +257 -73
  5. data/lib/mtg_card_maker/base_card.rb +24 -3
  6. data/lib/mtg_card_maker/cli.rb +69 -66
  7. data/lib/mtg_card_maker/color_scheme.rb +18 -23
  8. data/lib/mtg_card_maker/css_service.rb +72 -0
  9. data/lib/mtg_card_maker/fonts/GoudyBookletter1911.ttf +0 -0
  10. data/lib/mtg_card_maker/fonts/OFL.txt +92 -0
  11. data/lib/mtg_card_maker/fonts/goudy_bookletter_base64.txt +1 -0
  12. data/lib/mtg_card_maker/icon_filename_service.rb +118 -0
  13. data/lib/mtg_card_maker/icon_service.rb +87 -25
  14. data/lib/mtg_card_maker/icons/black.svg +6 -1
  15. data/lib/mtg_card_maker/icons/blue.svg +6 -1
  16. data/lib/mtg_card_maker/icons/colorless.svg +6 -1
  17. data/lib/mtg_card_maker/icons/double-digit.svg +6 -0
  18. data/lib/mtg_card_maker/icons/energy.svg +26 -0
  19. data/lib/mtg_card_maker/icons/green.svg +6 -1
  20. data/lib/mtg_card_maker/icons/hybrid/2-black.svg +19 -0
  21. data/lib/mtg_card_maker/icons/hybrid/2-blue.svg +19 -0
  22. data/lib/mtg_card_maker/icons/hybrid/2-green.svg +19 -0
  23. data/lib/mtg_card_maker/icons/hybrid/2-red.svg +19 -0
  24. data/lib/mtg_card_maker/icons/hybrid/2-white.svg +19 -0
  25. data/lib/mtg_card_maker/icons/hybrid/black-blue.svg +19 -0
  26. data/lib/mtg_card_maker/icons/hybrid/black-green.svg +19 -0
  27. data/lib/mtg_card_maker/icons/hybrid/black-red.svg +19 -0
  28. data/lib/mtg_card_maker/icons/hybrid/black-white.svg +19 -0
  29. data/lib/mtg_card_maker/icons/hybrid/blue-black.svg +19 -0
  30. data/lib/mtg_card_maker/icons/hybrid/blue-green.svg +19 -0
  31. data/lib/mtg_card_maker/icons/hybrid/blue-red.svg +19 -0
  32. data/lib/mtg_card_maker/icons/hybrid/blue-white.svg +19 -0
  33. data/lib/mtg_card_maker/icons/hybrid/colorless-black.svg +19 -0
  34. data/lib/mtg_card_maker/icons/hybrid/colorless-blue.svg +19 -0
  35. data/lib/mtg_card_maker/icons/hybrid/colorless-green.svg +19 -0
  36. data/lib/mtg_card_maker/icons/hybrid/colorless-red.svg +19 -0
  37. data/lib/mtg_card_maker/icons/hybrid/colorless-white.svg +19 -0
  38. data/lib/mtg_card_maker/icons/hybrid/green-black.svg +19 -0
  39. data/lib/mtg_card_maker/icons/hybrid/green-blue.svg +19 -0
  40. data/lib/mtg_card_maker/icons/hybrid/green-red.svg +19 -0
  41. data/lib/mtg_card_maker/icons/hybrid/green-white.svg +19 -0
  42. data/lib/mtg_card_maker/icons/hybrid/red-black.svg +19 -0
  43. data/lib/mtg_card_maker/icons/hybrid/red-blue.svg +19 -0
  44. data/lib/mtg_card_maker/icons/hybrid/red-green.svg +19 -0
  45. data/lib/mtg_card_maker/icons/hybrid/red-white.svg +19 -0
  46. data/lib/mtg_card_maker/icons/hybrid/white-black.svg +19 -0
  47. data/lib/mtg_card_maker/icons/hybrid/white-blue.svg +19 -0
  48. data/lib/mtg_card_maker/icons/hybrid/white-green.svg +19 -0
  49. data/lib/mtg_card_maker/icons/hybrid/white-red.svg +19 -0
  50. data/lib/mtg_card_maker/icons/phyrexian/black-blue.svg +19 -0
  51. data/lib/mtg_card_maker/icons/phyrexian/black-green.svg +19 -0
  52. data/lib/mtg_card_maker/icons/phyrexian/black-red.svg +19 -0
  53. data/lib/mtg_card_maker/icons/phyrexian/black-white.svg +19 -0
  54. data/lib/mtg_card_maker/icons/phyrexian/black.svg +6 -0
  55. data/lib/mtg_card_maker/icons/phyrexian/blue-black.svg +19 -0
  56. data/lib/mtg_card_maker/icons/phyrexian/blue-green.svg +19 -0
  57. data/lib/mtg_card_maker/icons/phyrexian/blue-red.svg +19 -0
  58. data/lib/mtg_card_maker/icons/phyrexian/blue-white.svg +19 -0
  59. data/lib/mtg_card_maker/icons/phyrexian/blue.svg +6 -0
  60. data/lib/mtg_card_maker/icons/phyrexian/colorless.svg +6 -0
  61. data/lib/mtg_card_maker/icons/phyrexian/green-black.svg +19 -0
  62. data/lib/mtg_card_maker/icons/phyrexian/green-blue.svg +19 -0
  63. data/lib/mtg_card_maker/icons/phyrexian/green-red.svg +19 -0
  64. data/lib/mtg_card_maker/icons/phyrexian/green-white.svg +19 -0
  65. data/lib/mtg_card_maker/icons/phyrexian/green.svg +6 -0
  66. data/lib/mtg_card_maker/icons/phyrexian/red-black.svg +19 -0
  67. data/lib/mtg_card_maker/icons/phyrexian/red-blue.svg +19 -0
  68. data/lib/mtg_card_maker/icons/phyrexian/red-green.svg +19 -0
  69. data/lib/mtg_card_maker/icons/phyrexian/red-white.svg +19 -0
  70. data/lib/mtg_card_maker/icons/phyrexian/red.svg +6 -0
  71. data/lib/mtg_card_maker/icons/phyrexian/white-black.svg +19 -0
  72. data/lib/mtg_card_maker/icons/phyrexian/white-blue.svg +19 -0
  73. data/lib/mtg_card_maker/icons/phyrexian/white-green.svg +19 -0
  74. data/lib/mtg_card_maker/icons/phyrexian/white-red.svg +19 -0
  75. data/lib/mtg_card_maker/icons/phyrexian/white.svg +6 -0
  76. data/lib/mtg_card_maker/icons/red.svg +6 -1
  77. data/lib/mtg_card_maker/icons/single-digit.svg +6 -0
  78. data/lib/mtg_card_maker/icons/snow.svg +24 -0
  79. data/lib/mtg_card_maker/icons/tap.svg +1 -0
  80. data/lib/mtg_card_maker/icons/tap_legacy.svg +45 -0
  81. data/lib/mtg_card_maker/icons/untap.svg +1 -0
  82. data/lib/mtg_card_maker/icons/white.svg +6 -1
  83. data/lib/mtg_card_maker/icons/x.svg +6 -0
  84. data/lib/mtg_card_maker/layer_config.rb +21 -46
  85. data/lib/mtg_card_maker/layers/border_layer.rb +4 -5
  86. data/lib/mtg_card_maker/layers/frame_layer.rb +1 -1
  87. data/lib/mtg_card_maker/layers/name_layer.rb +16 -8
  88. data/lib/mtg_card_maker/layers/power_layer.rb +1 -3
  89. data/lib/mtg_card_maker/layers/text_box_layer.rb +205 -51
  90. data/lib/mtg_card_maker/layers/type_line_layer.rb +2 -4
  91. data/lib/mtg_card_maker/logger.rb +62 -0
  92. data/lib/mtg_card_maker/mana_cost.rb +59 -128
  93. data/lib/mtg_card_maker/sprite_sheet_assets.rb +10 -55
  94. data/lib/mtg_card_maker/sprite_sheet_builder.rb +20 -8
  95. data/lib/mtg_card_maker/sprite_sheet_service.rb +22 -9
  96. data/lib/mtg_card_maker/svg_gradient_service.rb +11 -11
  97. data/lib/mtg_card_maker/symbol_replacement_service.rb +102 -0
  98. data/lib/mtg_card_maker/text_rendering_service.rb +7 -3
  99. data/lib/mtg_card_maker/version.rb +2 -2
  100. data/lib/mtg_card_maker.rb +9 -78
  101. metadata +79 -12
  102. data/lib/mtg_card_maker/fonts/Goudy Mediaeval DemiBold.ttf +0 -0
  103. data/lib/mtg_card_maker/fonts/goudy_base64.txt +0 -1
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg"
2
+ width="24px"
3
+ height="24px"
4
+ viewBox="0 0 600 600">
5
+ <g
6
+ transform="translate(-70.576066,118.21478)"
7
+ id="layer1">
8
+ <circle
9
+ cx="50.000999"
10
+ cy="50"
11
+ r="50"
12
+ transform="matrix(5.99994,0,0,6,70.576066,-118.21478)"
13
+ id="circle485"
14
+ style="fill:#c9c4be" />
15
+ <path d="M25.001,93.303 L33.505,89.209 L37.843,89.157 L40.198,85.077 L50.001,98.881 L59.803,85.077 L62.158,89.157 L66.496,89.208 L75.001,93.303 L75.708,83.891 L77.833,80.105 L75.479,76.027 L92.334,74.439 L85.279,59.05 L89.99,59.05 L92.203,55.318 L100,50 L92.203,44.681 L89.99,40.95 L85.279,40.95 L92.333,25.559 L75.479,23.973 L77.833,19.893 L75.707,16.111 L75,6.699 L66.496,10.792 L62.158,10.843 L59.803,14.922 L50,1.118 L40.198,14.922 L37.843,10.843 L33.505,10.792 L25,6.699 L24.293,16.111 L22.168,19.893 L24.523,23.973 L7.667,25.56 L14.722,40.95 L10.011,40.95 L7.798,44.682 L0,50 L7.797,55.318 L10.011,59.05 L14.722,59.05 L7.668,74.441 L24.523,76.027 L22.168,80.105 L24.293,83.889" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#0d0f0f" />
16
+ <path d="M68.609,86.282 L72.116,84.258 L72.116,80.212 L68.609,78.188 L55.088,52.938 L83.717,52.023 L87.223,54.048 L90.727,52.025 L90.727,47.976 L87.223,45.953 L83.717,47.976 L55.088,47.063 L68.611,21.813 L72.117,19.789 L72.117,15.743 L68.611,13.719 L65.107,15.742 L65.105,19.789 L50.001,44.125 L34.896,19.789 L34.895,15.741 L31.392,13.718 L27.885,15.743 L27.885,19.789 L31.391,21.813 L44.913,47.063 L16.285,47.976 L12.778,45.953 L9.275,47.975 L9.275,52.023 L12.778,54.047 L16.285,52.023 L44.913,52.938 L31.39,78.188 L27.885,80.211 L27.884,84.258 L31.39,86.281 L34.895,84.258 L34.895,80.211 L50,55.875 L65.105,80.211 L65.105,84.259" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#ffffff" />
17
+ <path d="M57.051,22.393 L50.001,34.681 L42.951,22.393 L50.001,10.104" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#ffffff" />
18
+ <path d="M29.617,30.09 L36.734,42.34 L22.567,42.301 L15.45,30.052" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#ffffff" />
19
+ <path d="M22.566,57.697 L36.733,57.659 L29.616,69.908 L15.449,69.947" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#ffffff" />
20
+ <path d="M42.949,77.607 L50,65.318 L57.05,77.607 L50,89.896" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#ffffff" />
21
+ <path d="M70.384,69.91 L63.268,57.66 L77.434,57.699 L84.551,69.947" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#ffffff" />
22
+ <path d="M77.436,42.303 L63.268,42.341 L70.385,30.092 L84.551,30.053" transform="matrix(5.99994,0,0,6,70.576066,-118.21478)" style="fill:#ffffff" />
23
+ </g>
24
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 600 600" version="1.1"><circle cx="300" cy="300" r="300" fill="#DDD"/><path d="m512 353.5-216 0 79.1-56.3c-29.4-23.3-63.4-35-101.9-35-19.3 0-32.5 3.7-39.5 11-7.1 7.4-10.7 20.7-10.7 39.9 0 53.2 27.4 110 82.2 170.4l-62.3 63.1c-72.5-88.2-108.8-167.1-108.8-236.5 0-41.6 12.5-74.7 37.6-99.3 25.1-24.6 58.5-36.9 100-36.9 50.7 0 106 19.3 165.9 57.8l46.4-79.1 28.2 200.8" fill="#111"/></svg>
@@ -0,0 +1,45 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+ <svg
5
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
6
+ xmlns:cc="http://creativecommons.org/ns#"
7
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
+ xmlns:svg="http://www.w3.org/2000/svg"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11
+ version="1.1"
12
+ width="600"
13
+ height="600"
14
+ id="svg5391">
15
+ <defs
16
+ id="defs5393" />
17
+ <metadata
18
+ id="metadata5396">
19
+ <rdf:RDF>
20
+ <cc:Work
21
+ rdf:about="">
22
+ <dc:format>image/svg+xml</dc:format>
23
+ <dc:type
24
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
25
+ <dc:title />
26
+ </cc:Work>
27
+ </rdf:RDF>
28
+ </metadata>
29
+ <g
30
+ transform="translate(-284.8627,49.643341)"
31
+ id="layer1">
32
+ <circle
33
+ cx="-475.125"
34
+ cy="365.06799"
35
+ r="50"
36
+ transform="matrix(6,0,0,6,3435.6127,-1940.0513)"
37
+ id="circle447"
38
+ style="fill:#c9c4be" />
39
+ <path
40
+ d="m 853.51868,285.44471 -63.42,76.37397 -15.53999,-15.52797 c 6.042,-15.24601 9.86998,-29.274 11.44199,-42.08402 1.57201,-12.79798 0.92399,-24.65399 -1.944,-35.60398 -0.28784,-1.42802 -0.63601,-2.802 -1.07999,-4.092 -0.426,-1.31403 -0.92999,-2.532 -1.51203,-3.67801 -2.29799,-6.88799 -5.75398,-13.37401 -10.36199,-19.404 -4.602,-6.06001 -11.20798,-13.38 -19.836,-22.00802 L 705.95469,173.2507 495.39067,383.8267 c -18.41398,18.396 -28.34397,34.374 -29.77799,47.87401 -1.44601,13.53597 5.17802,27.62999 19.85401,42.30597 l 6.042,5.16602 -16.39799,16.39199 -138.072,-138.07201 16.39198,-16.39198 6.9,6.88799 c 14.382,14.964 28.33199,21.73799 41.85599,20.28601 13.51803,-1.42802 29.34603,-11.22599 47.46602,-29.34599 L 660.64268,127.93269 613.6147,80.904705 c -13.23001,-12.654021 -24.522,-21.504022 -33.87003,-26.532022 -9.354,-5.039976 -20.21999,-7.127995 -32.57399,-6.257984 -1.73398,0 -3.53999,0.07213 -5.39998,0.210013 -1.872,0.149961 -3.816,0.365979 -5.82602,0.641982 -3.744,0.293884 -7.04999,0.714011 -9.91798,1.302014 -2.88003,0.575992 -5.616,0.995985 -8.20201,1.290003 -0.87001,0.293884 -1.65602,0.503997 -2.36999,0.641982 -0.72002,0.137884 -1.65602,0.22209 -2.80801,0.210013 -3.168,0.869977 -6.76802,1.878006 -10.78799,3.023986 -4.03802,1.134003 -9.06602,2.447993 -15.10802,3.876014 l -14.238,-15.108019 74.64002,-65.142004 306.36598,306.384027 z"
41
+ inkscape:connector-curvature="0"
42
+ id="path451"
43
+ style="fill:#0d0f0f" />
44
+ </g>
45
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 600 600" version="1.1"><circle cx="300" cy="300" r="300" fill="#111"/><path d="m473.7 145.7-75.9 44.5c37 57.4 55.4 110.4 55.4 159.1 0 39.9-11.8 71.9-35.4 95.9-23.6 24.1-55.3 36.1-95.2 36.1-66.7 0-142.3-34.8-226.9-104.3l60.6-59.9c57.9 52.6 112.4 78.8 163.5 78.8 18.5 0 31.2-3.4 38.3-10.2 7.1-6.8 10.6-19.5 10.6-37.9 0-37-11.2-69.6-33.6-97.8l-54 75.9 0-207.2 192.6 27" fill="#FFF"/></svg>
@@ -1 +1,6 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#111"><path d="M-6-420v-120h282v120H-6Zm299-162L185-691l84-84 109 108-85 85Zm127-102v-282h120v282H420Zm247 102-85-85 109-108 84 84-108 109Zm17 162v-120h282v120H684Zm-203.71 85Q420-335 377.5-377.21q-42.5-42.2-42.5-102.5 0-60.29 42.21-102.79 42.2-42.5 102.5-42.5 60.29 0 102.79 42.21 42.5 42.2 42.5 102.5 0 60.29-42.21 102.79-42.2 42.5-102.5 42.5ZM691-185 582-293l85-85 108 109-84 84Zm-422 0-84-84 108-109 85 85-109 108ZM420 6v-282h120V6H420Z"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
2
+ <circle cx='50%' cy='-50%' r='480' fill='#FEFDDF' />
3
+ <g transform='translate(70, -70) scale(0.85)'>
4
+ <path d="M-6-420v-120h282v120H-6Zm299-162L185-691l84-84 109 108-85 85Zm127-102v-282h120v282H420Zm247 102-85-85 109-108 84 84-108 109Zm17 162v-120h282v120H684Zm-203.71 85Q420-335 377.5-377.21q-42.5-42.2-42.5-102.5 0-60.29 42.21-102.79 42.2-42.5 102.5-42.5 60.29 0 102.79 42.21 42.5 42.2 42.5 102.5 0 60.29-42.21 102.79-42.2 42.5-102.5 42.5ZM691-185 582-293l85-85 108 109-84 84Zm-422 0-84-84 108-109 85 85-109 108ZM420 6v-282h120V6H420Z"/>
5
+ </g>
6
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-15 -15 30 30" width="24px">
2
+ <circle cx='0' cy='0' r='15' fill='#DDD' />
3
+ <text x='0' y='10' fill='#000' text-anchor='middle' font-weight='normal' font-size='30' font-family='serif'>
4
+ X
5
+ </text>
6
+ </svg>
@@ -27,10 +27,9 @@ module MtgCardMaker
27
27
  DEFAULT_CONFIG = {
28
28
  # Text rendering settings
29
29
  font_sizes: {
30
- name: 32,
30
+ name: 36,
31
31
  type: 22,
32
- description: 24,
33
- flavor_text: 18,
32
+ text_box: 24,
34
33
  power_area: 28,
35
34
  copyright: 14
36
35
  },
@@ -43,8 +42,8 @@ module MtgCardMaker
43
42
  char_width_multiplier: 0.42,
44
43
  css_classes: {
45
44
  card_name: 'card-name',
46
- card_type: 'card-type',
47
- card_description: 'card-description',
45
+ type_line: 'card-type',
46
+ rules_text: 'card-rules-text',
48
47
  flavor_text: 'card-flavor-text',
49
48
  power_area: 'card-power-toughness',
50
49
  copyright: 'card-copyright'
@@ -67,7 +66,7 @@ module MtgCardMaker
67
66
  y_offset: 8,
68
67
  width_ratio: 0.75
69
68
  },
70
- description: {
69
+ rules_text: {
71
70
  y_offset: 30,
72
71
  width_ratio: 1.0
73
72
  },
@@ -94,11 +93,16 @@ module MtgCardMaker
94
93
  }
95
94
  },
96
95
 
96
+ text_box: {
97
+ icon_size: 24,
98
+ hybrid_icon_size: 32
99
+ },
100
+
97
101
  # Mana cost settings
98
102
  mana_cost: {
99
- circle_radius: 15,
100
103
  circle_spacing: 35,
101
- icon_size: 24,
104
+ icon_size: 30,
105
+ hybrid_icon_size: 38,
102
106
  max_circles: 10,
103
107
  margin: 10
104
108
  },
@@ -125,21 +129,9 @@ module MtgCardMaker
125
129
 
126
130
  # Drop shadow settings
127
131
  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
132
+ dx: -1,
133
+ dy: 2.5,
134
+ std_deviation: 0
143
135
  }
144
136
  }.freeze
145
137
 
@@ -155,7 +147,7 @@ module MtgCardMaker
155
147
  @config = default_config_with_merge.deep_merge(custom_config)
156
148
  end
157
149
 
158
- # Font size getters
150
+ # Size getters
159
151
  def font_size(layer_type)
160
152
  config[:font_sizes][layer_type]
161
153
  end
@@ -183,6 +175,11 @@ module MtgCardMaker
183
175
  config[:frames][:corner_radius][layer_type] || { x: 5, y: 5 }
184
176
  end
185
177
 
178
+ # Text box getters
179
+ def text_box_config
180
+ config[:text_box]
181
+ end
182
+
186
183
  # Mana cost getters
187
184
  def mana_cost_config
188
185
  config[:mana_cost]
@@ -237,28 +234,6 @@ module MtgCardMaker
237
234
  config[:drop_shadow]
238
235
  end
239
236
 
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
237
  # Convenience methods for common calculations
263
238
  def text_width(base_width, layer_type = nil)
264
239
  if layer_type && positioning(layer_type)[:width_ratio]
@@ -32,7 +32,7 @@ module MtgCardMaker
32
32
  SUPPORTED_COLORS = {
33
33
  white: '#EEE',
34
34
  black: '#000',
35
- silver: :colorless,
35
+ silver: :silver,
36
36
  gold: :gold
37
37
  }.freeze
38
38
 
@@ -77,7 +77,7 @@ module MtgCardMaker
77
77
  render_white_frame(corners)
78
78
  when :black
79
79
  render_black_frame(corners)
80
- when :colorless, :gold
80
+ when :silver, :gold
81
81
  render_metallic_frame(corners)
82
82
  else
83
83
  raise ArgumentError, "Unsupported border color: #{color.inspect}. Supported: white, black, silver, gold."
@@ -102,7 +102,6 @@ module MtgCardMaker
102
102
  rx: corners[:x], ry: corners[:y],
103
103
  fill: '#EEE',
104
104
  mask: "url(##{@mask_id})"
105
- SvgGradientService.define_all_gradients(svg, color_scheme)
106
105
  render_metallic_elements(
107
106
  mask: @mask_id,
108
107
  corners: corners,
@@ -119,7 +118,7 @@ module MtgCardMaker
119
118
  end
120
119
 
121
120
  def color_key_for(color)
122
- return :colorless if color == :silver
121
+ return :silver if color == :silver
123
122
  return :gold if color == :gold
124
123
 
125
124
  color
@@ -143,7 +142,7 @@ module MtgCardMaker
143
142
 
144
143
  def render_copyright_texts(layer_config)
145
144
  copyright_texts = [
146
- '© 2025 Joe Sharp. Some rights reserved.',
145
+ 'MTG Card Maker by Joe Sharp',
147
146
  'Portions of the materials used are property of Wizards of the Coast.',
148
147
  '© Wizards of the Coast LLC'
149
148
  ]
@@ -9,6 +9,7 @@ module MtgCardMaker
9
9
  class FrameLayer < BaseLayer
10
10
  include MetallicRenderer
11
11
  include LayerInitializer
12
+
12
13
  attr_reader :color_scheme, :mask_id, :layer_config
13
14
 
14
15
  def initialize(dimensions:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME, mask_id: 'artWindowMask')
@@ -21,7 +22,6 @@ module MtgCardMaker
21
22
 
22
23
  # Render the frame with a gradient background
23
24
  def render
24
- SvgGradientService.define_all_gradients(svg, color_scheme)
25
25
  if metallic_properties?
26
26
  render_metallic_frame
27
27
  else
@@ -7,6 +7,7 @@ module MtgCardMaker
7
7
  # This is the area that surrounds the name and cost of the card
8
8
  class NameLayer < BaseLayer
9
9
  include LayerInitializer
10
+
10
11
  attr_reader :name, :cost, :color_scheme
11
12
 
12
13
  def initialize(dimensions:, name:, cost:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
@@ -18,9 +19,6 @@ module MtgCardMaker
18
19
  end
19
20
 
20
21
  def render
21
- # Ensure gradients are defined for this color scheme
22
- SvgGradientService.define_all_gradients(svg, color_scheme)
23
-
24
22
  svg.g do
25
23
  render_name_area
26
24
  render_card_name
@@ -42,13 +40,16 @@ module MtgCardMaker
42
40
  rx: corners[:x], ry: corners[:y], mask: "url(##{@mask_id})"
43
41
  end
44
42
 
43
+ def layer_config
44
+ @layer_config ||= LayerConfig.default
45
+ end
46
+
45
47
  def render_card_name
46
- layer_config = LayerConfig.default
47
48
  text_service = TextRenderingService.new(
48
49
  text: name,
49
50
  layer_config: layer_config,
50
- x: layer_config.text_x_position(x),
51
- y: layer_config.text_y_position(y, :name_area, height),
51
+ x: text_x_position,
52
+ y: text_y_position,
52
53
  font_size: layer_config.font_size(:name),
53
54
  available_width: layer_config.text_width(width, :name_area),
54
55
  css_class: layer_config.css_class(:card_name)
@@ -59,6 +60,14 @@ module MtgCardMaker
59
60
  end
60
61
  end
61
62
 
63
+ def text_x_position
64
+ layer_config.text_x_position(x)
65
+ end
66
+
67
+ def text_y_position
68
+ layer_config.text_y_position(y, :name_area, height)
69
+ end
70
+
62
71
  def render_mana_cost
63
72
  mana_cost = ManaCost.new(cost)
64
73
  cost_x = cost_position_x(mana_cost)
@@ -69,9 +78,8 @@ module MtgCardMaker
69
78
  end
70
79
 
71
80
  def cost_position_x(mana_cost)
72
- layer_config = LayerConfig.default
73
81
  config = layer_config.mana_cost_config
74
- base_x = x + width - config[:margin] - config[:circle_radius]
82
+ base_x = x + width - config[:margin] - (config[:icon_size] / 2)
75
83
  base_x - (config[:circle_spacing] * (mana_cost.elements.length - 1))
76
84
  end
77
85
 
@@ -5,6 +5,7 @@ module MtgCardMaker
5
5
  # This is the area that surrounds the power and toughness of the card
6
6
  class PowerLayer < BaseLayer
7
7
  include LayerInitializer
8
+
8
9
  attr_reader :power, :toughness, :color_scheme
9
10
 
10
11
  def initialize(dimensions:, power:, toughness:, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
@@ -19,9 +20,6 @@ module MtgCardMaker
19
20
  # Don't render if power or toughness are nil, empty, or invalid
20
21
  return if power.nil? || toughness.nil? || power.to_s.strip.empty? || toughness.to_s.strip.empty?
21
22
 
22
- # Ensure gradients are defined for this color scheme
23
- SvgGradientService.define_all_gradients(svg, color_scheme)
24
-
25
23
  layer_config = LayerConfig.default
26
24
  stroke_width = layer_config.stroke_width
27
25
  corners = layer_config.corner_radius(:power)
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MtgCardMaker
4
+ # Text limits for different scenarios
5
+ MAX_RULES_LINES = 9
6
+ MAX_FLAVOR_LINES = 7 # Two less because one line of rules is required, plus the separator
7
+
4
8
  # TextBoxLayer is a specialized layer for the rules and flavor text
9
+ # with bidirectional text flow from a dynamic separator
5
10
  class TextBoxLayer < BaseLayer
6
11
  include LayerInitializer
7
- attr_reader :rules_text, :flavor_text, :color_scheme
12
+
13
+ attr_reader :rules_text, :flavor_text, :color_scheme, :font_size, :text_width
8
14
 
9
15
  def initialize(dimensions:, rules_text:, flavor_text: nil, color: nil, color_scheme: DEFAULT_COLOR_SCHEME)
10
16
  frame_color = initialize_layer_color(color, color_scheme, :background_color)
@@ -12,96 +18,244 @@ module MtgCardMaker
12
18
  @rules_text = rules_text
13
19
  @flavor_text = flavor_text
14
20
  @color_scheme = color_scheme
21
+ @font_size = layer_config.font_size(:text_box)
22
+ @text_width = layer_config.text_width(width, :rules_text)
15
23
  end
16
24
 
17
- # Render the rules text and flavor text in a text box with a background and highlight
25
+ # Render the rules text and flavor text in a text box with bidirectional flow
18
26
  def render
19
- # Ensure gradients are defined for this color scheme
20
- SvgGradientService.define_all_gradients(svg, color_scheme)
21
-
22
27
  render_text_box
23
28
  end
24
29
 
25
30
  private
26
31
 
27
- def render_text_box
28
- layer_config = LayerConfig.default
29
- stroke_width = layer_config.stroke_width
32
+ attr_reader :text_box_lines
30
33
 
34
+ def render_text_box
31
35
  svg.g do
32
- # Text box background with mask for transparent window
33
- svg.rect x: x, y: y, width: width, height: height,
34
- fill: "url(##{SvgGradientService.description_gradient_id(color_scheme)})",
35
- stroke: color_scheme.primary_color,
36
- stroke_width: stroke_width
37
-
36
+ render_background
38
37
  render_text_content
39
38
  end
40
39
  end
41
40
 
41
+ def render_background
42
+ svg.rect x: x, y: y, width: width, height: height,
43
+ fill: "url(##{SvgGradientService.text_box_gradient_id(color_scheme)})",
44
+ stroke: color_scheme.primary_color,
45
+ stroke_width: layer_config.stroke_width
46
+ end
47
+
42
48
  def render_text_content
43
- render_rules_text
44
- render_flavor_text if flavor_text
49
+ text_box_lines = create_text_box_lines
50
+
51
+ # Render separator
52
+ separator = text_box_lines.separator_line
53
+ render_separator(separator[:y_pos]) if separator
54
+
55
+ # Render text lines
56
+ text_box_lines.text_lines.each do |line|
57
+ render_text_line(line[:text], line[:y_pos], line[:type])
58
+ end
45
59
  end
46
60
 
47
- def render_rules_text
48
- layer_config = LayerConfig.default
49
- text_service = create_text_service(layer_config, rules_text, :description, :card_description)
50
- render_text_lines(text_service)
61
+ def create_text_box_lines
62
+ @text_box_lines = TextBoxLines.new(x: x, y: y, width: width, height: height)
63
+
64
+ if flavor_text_present?
65
+ setup_bidirectional_text
66
+ else
67
+ setup_rules_only_text
68
+ end
69
+
70
+ @text_box_lines
51
71
  end
52
72
 
53
- def render_flavor_text
54
- return unless flavor_text && !flavor_text.strip.empty?
73
+ def setup_bidirectional_text
74
+ rules_lines = calculate_rules_lines
75
+ flavor_lines = calculate_flavor_lines(rules_lines.length)
55
76
 
56
- render_flavor_separator
57
- layer_config = LayerConfig.default
58
- text_service = create_text_service(layer_config, flavor_text, :flavor_text, :flavor_text)
59
- render_text_lines(text_service)
77
+ # Calculate separator line: first line after rules text, but never lower than line 4 (0-based)
78
+ separator_line = [rules_lines.length, 4].max
79
+
80
+ # Place rules text upward from separator
81
+ place_text_lines(rules_lines.reverse, separator_line, direction: :upward, type: :rules_text)
82
+
83
+ # Place separator
84
+ text_box_lines.set_line(separator_line, type: :separator)
85
+
86
+ # Place flavor text downward from separator
87
+ place_text_lines(flavor_lines, separator_line, direction: :downward, type: :flavor_text)
60
88
  end
61
89
 
62
- def create_text_service(layer_config, text, text_type, css_class)
63
- TextRenderingService.new(
64
- text: text,
65
- layer_config: layer_config,
66
- x: layer_config.text_x_position(x),
67
- y: calculate_text_y_position(layer_config, text_type),
68
- font_size: layer_config.font_size(text_type),
69
- color: color_scheme.text_color,
70
- available_width: layer_config.text_width(width, text_type),
71
- css_class: layer_config.css_class(css_class)
72
- )
90
+ def setup_rules_only_text
91
+ rules_lines = calculate_rules_lines
92
+
93
+ # Center the rules text in the available lines
94
+ start_line = (MAX_RULES_LINES - rules_lines.length) / 2
95
+
96
+ place_text_lines(rules_lines, start_line, direction: :forward, type: :rules_text)
73
97
  end
74
98
 
75
- def calculate_text_y_position(layer_config, text_type)
76
- if text_type == :flavor_text
77
- y + height - layer_config.positioning(:flavor_text)[:y_offset]
99
+ def place_text_lines(lines, start_line, direction:, type:)
100
+ lines.each_with_index do |line, index|
101
+ line_number = calculate_line_number(start_line, index, direction)
102
+ break if line_number.negative? || line_number >= MAX_RULES_LINES
103
+
104
+ text_box_lines.set_line(line_number, text: line, type: type)
105
+ end
106
+ end
107
+
108
+ def calculate_line_number(start_line, index, direction)
109
+ case direction
110
+ when :upward
111
+ start_line - index - 1
112
+ when :downward
113
+ start_line + index + 1
78
114
  else
79
- layer_config.text_y_position(y, :description)
115
+ start_line + index
80
116
  end
81
117
  end
82
118
 
83
- def render_text_lines(text_service)
84
- text_service.wrapped_text_lines.each do |line, attrs|
85
- svg.text line, attrs
119
+ def render_text_line(line, y_pos, text_type)
120
+ if symbol_service.contains_symbols?(line)
121
+ render_line_with_symbols(line, y_pos)
122
+ else
123
+ svg.text line, {
124
+ x: layer_config.text_x_position(x),
125
+ y: y_pos,
126
+ fill: color_scheme.text_color,
127
+ font_size: font_size,
128
+ class: layer_config.css_class(text_type)
129
+ }
86
130
  end
87
131
  end
88
132
 
89
- def render_flavor_separator
90
- svg.line(**separator_config)
133
+ def render_line_with_symbols(line, y_pos)
134
+ html_content = symbol_service.render_line_with_symbols(line, text_width)
135
+
136
+ # Use foreignObject with HTML for proper text/symbol alignment
137
+ svg.foreignObject x: layer_config.text_x_position(x),
138
+ y: y_pos - font_size, # Ensure symbols don't throw off the line
139
+ width: text_width,
140
+ height: font_size * 2 do
141
+ svg << html_content
142
+ end
91
143
  end
92
144
 
93
- def separator_config
94
- layer_config = LayerConfig.default
95
- separator_offset = layer_config.positioning(:flavor_text)[:separator_offset]
96
- separator_y = y + height - separator_offset
97
- {
145
+ def symbol_service
146
+ @symbol_service ||= SymbolReplacementService.new
147
+ end
148
+
149
+ def render_separator(separator_y)
150
+ svg.line(
98
151
  x1: layer_config.text_x_position(x),
99
152
  y1: separator_y,
100
153
  x2: x + width - layer_config.horizontal_padding,
101
154
  y2: separator_y,
102
155
  stroke: color_scheme.primary_color,
103
156
  stroke_width: 1
157
+ )
158
+ end
159
+
160
+ def calculate_rules_lines
161
+ return [] if rules_text.nil? || rules_text.strip.empty?
162
+
163
+ text_service = create_text_service(rules_text, :rules_text)
164
+ text_service.wrapped_text_lines.first(MAX_RULES_LINES).map(&:first)
165
+ end
166
+
167
+ def calculate_flavor_lines(rules_line_count)
168
+ return [] unless flavor_text_present?
169
+
170
+ text_service = create_text_service(flavor_text, :flavor_text)
171
+ max_flavor_lines = [(MAX_FLAVOR_LINES - rules_line_count + 1), 1].max
172
+ text_service.wrapped_text_lines.first(max_flavor_lines).map(&:first)
173
+ end
174
+
175
+ def create_text_service(text, text_type)
176
+ TextRenderingService.new(
177
+ text: text,
178
+ layer_config: layer_config,
179
+ x: layer_config.text_x_position(x),
180
+ y: 0, # Will be calculated per line
181
+ font_size: font_size,
182
+ color: color_scheme.text_color,
183
+ available_width: text_width,
184
+ css_class: layer_config.css_class(text_type)
185
+ )
186
+ end
187
+
188
+ def layer_config
189
+ @layer_config ||= LayerConfig.default
190
+ end
191
+
192
+ def flavor_text_present?
193
+ flavor_text && !flavor_text.strip.empty?
194
+ end
195
+ end
196
+
197
+ # TextBoxLines manages the positioning and content of the MAX_RULES_LINES lines in a text box
198
+ # Each line can contain rules text, flavor text, or be a separator
199
+ class TextBoxLines
200
+ attr_reader :x, :y, :width, :height
201
+
202
+ def initialize(x:, y:, width:, height:)
203
+ @width = width
204
+ @height = height
205
+ @lines = Array.new(MAX_RULES_LINES) { { text: nil, type: :rules_text } }
206
+ @line_spacing = height / 10.0
207
+ @x = x
208
+ @y = y + (@line_spacing / 3)
209
+ calculate_line_positions
210
+ end
211
+
212
+ # Set the content and type for a specific line (0-8)
213
+ def set_line(line_number, text: nil, type: :rules_text)
214
+ @lines[line_number] = { text: text, type: type }
215
+ end
216
+
217
+ # Get the content, type, and y position for a specific line (0-8)
218
+ def get_line(line_number)
219
+ line_data = @lines[line_number]
220
+ {
221
+ text: line_data[:text],
222
+ type: line_data[:type],
223
+ y_pos: @line_positions[line_number]
104
224
  }
105
225
  end
226
+
227
+ # Get all lines as an array of hashes with text, type, and y_pos
228
+ def all_lines
229
+ @lines.map.with_index do |line_data, index|
230
+ {
231
+ text: line_data[:text],
232
+ type: line_data[:type],
233
+ y_pos: @line_positions[index]
234
+ }
235
+ end
236
+ end
237
+
238
+ # Get all lines except separators and empty lines
239
+ def text_lines
240
+ all_lines.reject { |line| line[:type] == :separator || line[:text].nil? || line[:text].strip.empty? }
241
+ end
242
+
243
+ def separator_line
244
+ separator = all_lines.find { |line| line[:type] == :separator }
245
+ # Position the separator in the middle of its line
246
+ separator[:y_pos] -= (@line_spacing / 3) if separator
247
+ separator
248
+ end
249
+
250
+ private
251
+
252
+ def calculate_line_positions
253
+ # Calculate MAX_RULES_LINES equally distributed vertical positions within the text box
254
+ # Starting from the top of the text box (y) and ending at the bottom (y + height)
255
+ last_line = MAX_RULES_LINES - 1
256
+ @line_positions = (0..last_line).map do |line_num|
257
+ @y + (@line_spacing * (line_num + 1))
258
+ end
259
+ end
106
260
  end
107
261
  end