CardPainter.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. /*
  2. * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
  3. * Copyright (c) 2022, the SerenityOS developers.
  4. * Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
  5. *
  6. * SPDX-License-Identifier: BSD-2-Clause
  7. */
  8. #include "CardPainter.h"
  9. #include <AK/Array.h>
  10. #include <AK/GenericShorthands.h>
  11. #include <LibConfig/Client.h>
  12. #include <LibGfx/Font/Font.h>
  13. #include <LibGfx/Font/FontDatabase.h>
  14. namespace Cards {
  15. CardPainter& CardPainter::the()
  16. {
  17. static CardPainter s_card_painter;
  18. return s_card_painter;
  19. }
  20. CardPainter::CardPainter()
  21. {
  22. m_background_image_path = MUST(String::from_deprecated_string(Config::read_string("Games"sv, "Cards"sv, "CardBackImage"sv, "/res/graphics/cards/backs/buggie-deck.png"sv)));
  23. set_front_images_set_name(MUST(String::from_deprecated_string(Config::read_string("Games"sv, "Cards"sv, "CardFrontImages"sv, "Classic"sv))));
  24. }
  25. static constexpr Gfx::CharacterBitmap s_diamond {
  26. " # "
  27. " ### "
  28. " ##### "
  29. " ####### "
  30. "#########"
  31. " ####### "
  32. " ##### "
  33. " ### "
  34. " # "sv,
  35. 9, 9
  36. };
  37. static constexpr Gfx::CharacterBitmap s_heart {
  38. " # # "
  39. " ### ### "
  40. "#########"
  41. "#########"
  42. "#########"
  43. " ####### "
  44. " ##### "
  45. " ### "
  46. " # "sv,
  47. 9, 9
  48. };
  49. static constexpr Gfx::CharacterBitmap s_spade {
  50. " # "
  51. " ### "
  52. " ##### "
  53. " ####### "
  54. "#########"
  55. "#########"
  56. " ## # ## "
  57. " ### "
  58. " ### "sv,
  59. 9, 9
  60. };
  61. static constexpr Gfx::CharacterBitmap s_club {
  62. " ### "
  63. " ##### "
  64. " ##### "
  65. "## ### ##"
  66. "#########"
  67. "#########"
  68. " ## # ## "
  69. " ### "
  70. " ### "sv,
  71. 9, 9
  72. };
  73. NonnullRefPtr<Gfx::Bitmap> CardPainter::card_front(Suit suit, Rank rank)
  74. {
  75. auto suit_id = to_underlying(suit);
  76. auto rank_id = to_underlying(rank);
  77. auto& existing_bitmap = m_cards[suit_id][rank_id];
  78. if (!existing_bitmap.is_null())
  79. return *existing_bitmap;
  80. m_cards[suit_id][rank_id] = create_card_bitmap();
  81. paint_card_front(*m_cards[suit_id][rank_id], suit, rank);
  82. return *m_cards[suit_id][rank_id];
  83. }
  84. NonnullRefPtr<Gfx::Bitmap> CardPainter::card_back()
  85. {
  86. if (!m_card_back.is_null())
  87. return *m_card_back;
  88. m_card_back = create_card_bitmap();
  89. paint_card_back(*m_card_back);
  90. return *m_card_back;
  91. }
  92. NonnullRefPtr<Gfx::Bitmap> CardPainter::card_front_highlighted(Suit suit, Rank rank)
  93. {
  94. auto suit_id = to_underlying(suit);
  95. auto rank_id = to_underlying(rank);
  96. auto& existing_bitmap = m_cards_highlighted[suit_id][rank_id];
  97. if (!existing_bitmap.is_null())
  98. return *existing_bitmap;
  99. m_cards_highlighted[suit_id][rank_id] = create_card_bitmap();
  100. paint_highlighted_card(*m_cards_highlighted[suit_id][rank_id], card_front(suit, rank));
  101. return *m_cards_highlighted[suit_id][rank_id];
  102. }
  103. NonnullRefPtr<Gfx::Bitmap> CardPainter::card_front_inverted(Suit suit, Rank rank)
  104. {
  105. auto suit_id = to_underlying(suit);
  106. auto rank_id = to_underlying(rank);
  107. auto& existing_bitmap = m_cards_inverted[suit_id][rank_id];
  108. if (!existing_bitmap.is_null())
  109. return *existing_bitmap;
  110. m_cards_inverted[suit_id][rank_id] = create_card_bitmap();
  111. paint_inverted_card(*m_cards_inverted[suit_id][rank_id], card_front(suit, rank));
  112. return *m_cards_inverted[suit_id][rank_id];
  113. }
  114. NonnullRefPtr<Gfx::Bitmap> CardPainter::card_back_inverted()
  115. {
  116. if (!m_card_back_inverted.is_null())
  117. return *m_card_back_inverted;
  118. m_card_back_inverted = create_card_bitmap();
  119. paint_inverted_card(card_back(), *m_card_back_inverted);
  120. return *m_card_back_inverted;
  121. }
  122. void CardPainter::set_background_image_path(StringView path)
  123. {
  124. if (m_background_image_path == path)
  125. return;
  126. m_background_image_path = MUST(String::from_utf8(path));
  127. if (!m_card_back.is_null())
  128. paint_card_back(*m_card_back);
  129. if (!m_card_back_inverted.is_null())
  130. paint_inverted_card(*m_card_back_inverted, *m_card_back);
  131. }
  132. void CardPainter::set_front_images_set_name(AK::StringView path)
  133. {
  134. if (m_front_images_set_name == path)
  135. return;
  136. m_front_images_set_name = MUST(String::from_utf8(path));
  137. if (m_front_images_set_name.is_empty()) {
  138. for (auto& pip_bitmap : m_suit_pips)
  139. pip_bitmap = nullptr;
  140. for (auto& pip_bitmap : m_suit_pips_flipped_vertically)
  141. pip_bitmap = nullptr;
  142. } else {
  143. auto diamond = Gfx::Bitmap::load_from_file(MUST(String::formatted("/res/graphics/cards/fronts/{}/diamond.png", m_front_images_set_name))).release_value_but_fixme_should_propagate_errors();
  144. m_suit_pips[to_underlying(Suit::Diamonds)] = diamond;
  145. m_suit_pips_flipped_vertically[to_underlying(Suit::Diamonds)] = diamond->flipped(Gfx::Orientation::Vertical).release_value_but_fixme_should_propagate_errors();
  146. auto club = Gfx::Bitmap::load_from_file(MUST(String::formatted("/res/graphics/cards/fronts/{}/club.png", m_front_images_set_name))).release_value_but_fixme_should_propagate_errors();
  147. m_suit_pips[to_underlying(Suit::Clubs)] = club;
  148. m_suit_pips_flipped_vertically[to_underlying(Suit::Clubs)] = club->flipped(Gfx::Orientation::Vertical).release_value_but_fixme_should_propagate_errors();
  149. auto heart = Gfx::Bitmap::load_from_file(MUST(String::formatted("/res/graphics/cards/fronts/{}/heart.png", m_front_images_set_name))).release_value_but_fixme_should_propagate_errors();
  150. m_suit_pips[to_underlying(Suit::Hearts)] = heart;
  151. m_suit_pips_flipped_vertically[to_underlying(Suit::Hearts)] = heart->flipped(Gfx::Orientation::Vertical).release_value_but_fixme_should_propagate_errors();
  152. auto spade = Gfx::Bitmap::load_from_file(MUST(String::formatted("/res/graphics/cards/fronts/{}/spade.png", m_front_images_set_name))).release_value_but_fixme_should_propagate_errors();
  153. m_suit_pips[to_underlying(Suit::Spades)] = spade;
  154. m_suit_pips_flipped_vertically[to_underlying(Suit::Spades)] = spade->flipped(Gfx::Orientation::Vertical).release_value_but_fixme_should_propagate_errors();
  155. }
  156. // Clear all bitmaps using front images
  157. for (auto& suit_array : m_cards) {
  158. for (auto& card_bitmap : suit_array)
  159. card_bitmap = nullptr;
  160. }
  161. for (auto& suit_array : m_cards_highlighted) {
  162. for (auto& card_bitmap : suit_array)
  163. card_bitmap = nullptr;
  164. }
  165. }
  166. void CardPainter::set_background_color(Color background_color)
  167. {
  168. m_background_color = background_color;
  169. // Clear any cached card bitmaps that depend on the background color.
  170. for (auto& suit_array : m_cards_highlighted) {
  171. for (auto& rank_array : suit_array)
  172. rank_array = nullptr;
  173. }
  174. }
  175. NonnullRefPtr<Gfx::Bitmap> CardPainter::create_card_bitmap()
  176. {
  177. return Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { Card::width, Card::height }).release_value_but_fixme_should_propagate_errors();
  178. }
  179. void CardPainter::paint_card_front_pips(Gfx::Bitmap& bitmap, Suit suit, Rank rank)
  180. {
  181. Gfx::Painter painter { bitmap };
  182. auto& pip_bitmap = m_suit_pips[to_underlying(suit)];
  183. auto& pip_bitmap_flipped_vertically = m_suit_pips_flipped_vertically[to_underlying(suit)];
  184. struct Pip {
  185. int x;
  186. int y;
  187. bool flip_vertically;
  188. };
  189. auto paint_pips = [&](Span<Pip> pips) {
  190. for (auto& pip : pips) {
  191. auto& bitmap = pip.flip_vertically ? pip_bitmap_flipped_vertically : pip_bitmap;
  192. painter.blit({ pip.x - bitmap->width() / 2, pip.y - bitmap->height() / 2 }, *bitmap, bitmap->rect());
  193. }
  194. };
  195. constexpr int column_left = Card::width * 1 / 3;
  196. constexpr int column_middle = Card::width * 1 / 2;
  197. constexpr int column_right = Card::width - column_left;
  198. constexpr int row_top = Card::height / 6;
  199. constexpr int row_middle = Card::height / 2;
  200. constexpr int row_bottom = Card::height - row_top - 1;
  201. constexpr int row_2_of_4 = row_top + (row_bottom - row_top) * 1 / 3;
  202. constexpr int row_3_of_4 = Card::height - row_2_of_4 - 1;
  203. constexpr int row_2_of_5 = row_top + (row_bottom - row_top) * 1 / 4;
  204. constexpr int row_4_of_5 = Card::height - row_2_of_5 - 1;
  205. constexpr int row_2_of_7 = row_top + (row_bottom - row_top) * 1 / 6;
  206. constexpr int row_6_of_7 = Card::height - row_2_of_7 - 1;
  207. switch (rank) {
  208. case Rank::Ace:
  209. paint_pips(Array<Pip, 1>({ Pip { column_middle, row_middle, false } }));
  210. break;
  211. case Rank::Two:
  212. paint_pips(Array<Pip, 2>({ { column_middle, row_top, false },
  213. { column_middle, row_bottom, true } }));
  214. break;
  215. case Rank::Three:
  216. paint_pips(Array<Pip, 3>({ { column_middle, row_top, false },
  217. { column_middle, row_middle, false },
  218. { column_middle, row_bottom, true } }));
  219. break;
  220. case Rank::Four:
  221. paint_pips(Array<Pip, 4>({ { column_left, row_top, false },
  222. { column_right, row_top, false },
  223. { column_left, row_bottom, true },
  224. { column_right, row_bottom, true } }));
  225. break;
  226. case Rank::Five:
  227. paint_pips(Array<Pip, 5>({ { column_left, row_top, false },
  228. { column_right, row_top, false },
  229. { column_middle, row_middle, false },
  230. { column_left, row_bottom, true },
  231. { column_right, row_bottom, true } }));
  232. break;
  233. case Rank::Six:
  234. paint_pips(Array<Pip, 6>({ { column_left, row_top, false },
  235. { column_right, row_top, false },
  236. { column_left, row_middle, false },
  237. { column_right, row_middle, false },
  238. { column_left, row_bottom, true },
  239. { column_right, row_bottom, true } }));
  240. break;
  241. case Rank::Seven:
  242. paint_pips(Array<Pip, 7>({ { column_left, row_top, false },
  243. { column_right, row_top, false },
  244. { column_middle, row_2_of_5, false },
  245. { column_left, row_middle, false },
  246. { column_right, row_middle, false },
  247. { column_left, row_bottom, true },
  248. { column_right, row_bottom, true } }));
  249. break;
  250. case Rank::Eight:
  251. paint_pips(Array<Pip, 8>({ { column_left, row_top, false },
  252. { column_right, row_top, false },
  253. { column_middle, row_2_of_5, false },
  254. { column_left, row_middle, false },
  255. { column_right, row_middle, false },
  256. { column_middle, row_4_of_5, true },
  257. { column_left, row_bottom, true },
  258. { column_right, row_bottom, true } }));
  259. break;
  260. case Rank::Nine:
  261. paint_pips(Array<Pip, 9>({ { column_left, row_top, false },
  262. { column_right, row_top, false },
  263. { column_left, row_2_of_4, false },
  264. { column_right, row_2_of_4, false },
  265. { column_middle, row_middle, false },
  266. { column_left, row_3_of_4, true },
  267. { column_right, row_3_of_4, true },
  268. { column_left, row_bottom, true },
  269. { column_right, row_bottom, true } }));
  270. break;
  271. case Rank::Ten:
  272. paint_pips(Array<Pip, 10>({ { column_left, row_top, false },
  273. { column_right, row_top, false },
  274. { column_middle, row_2_of_7, false },
  275. { column_left, row_2_of_4, false },
  276. { column_right, row_2_of_4, false },
  277. { column_left, row_3_of_4, true },
  278. { column_right, row_3_of_4, true },
  279. { column_middle, row_6_of_7, true },
  280. { column_left, row_bottom, true },
  281. { column_right, row_bottom, true } }));
  282. break;
  283. case Rank::Jack:
  284. case Rank::Queen:
  285. case Rank::King:
  286. case Rank::__Count:
  287. break;
  288. }
  289. }
  290. void CardPainter::paint_card_front(Gfx::Bitmap& bitmap, Cards::Suit suit, Cards::Rank rank)
  291. {
  292. auto const suit_color = (suit == Suit::Diamonds || suit == Suit::Hearts) ? Color::Red : Color::Black;
  293. auto const& suit_symbol = [&]() -> Gfx::CharacterBitmap const& {
  294. switch (suit) {
  295. case Suit::Diamonds:
  296. return s_diamond;
  297. case Suit::Clubs:
  298. return s_club;
  299. case Suit::Spades:
  300. return s_spade;
  301. case Suit::Hearts:
  302. return s_heart;
  303. default:
  304. VERIFY_NOT_REACHED();
  305. }
  306. }();
  307. Gfx::Painter painter { bitmap };
  308. auto paint_rect = bitmap.rect();
  309. auto& font = Gfx::FontDatabase::default_font().bold_variant();
  310. painter.fill_rect_with_rounded_corners(paint_rect, Color::Black, Card::card_radius);
  311. paint_rect.shrink(2, 2);
  312. painter.fill_rect_with_rounded_corners(paint_rect, Color::White, Card::card_radius - 1);
  313. paint_rect.set_height(paint_rect.height() / 2);
  314. paint_rect.shrink(10, 6);
  315. auto text_rect = Gfx::IntRect { 1, 6, font.width_rounded_up("10"sv), font.pixel_size_rounded_up() };
  316. painter.draw_text(text_rect, card_rank_label(rank), font, Gfx::TextAlignment::Center, suit_color);
  317. painter.draw_bitmap(
  318. { text_rect.x() + (text_rect.width() - suit_symbol.size().width()) / 2, text_rect.bottom() + 4 },
  319. suit_symbol, suit_color);
  320. for (int y = Card::height / 2; y < Card::height; ++y) {
  321. for (int x = 0; x < Card::width; ++x)
  322. bitmap.set_pixel(x, y, bitmap.get_pixel(Card::width - x - 1, Card::height - y - 1));
  323. }
  324. if (!m_front_images_set_name.is_empty()) {
  325. // Paint pips for number cards except the ace of spades
  326. if (!first_is_one_of(rank, Rank::Ace, Rank::Jack, Rank::Queen, Rank::King)
  327. || (rank == Rank::Ace && suit != Suit::Spades)) {
  328. paint_card_front_pips(bitmap, suit, rank);
  329. } else {
  330. // Paint pictures for royal cards and ace of spades
  331. StringView rank_name;
  332. switch (rank) {
  333. case Rank::Ace:
  334. rank_name = "ace"sv;
  335. break;
  336. case Rank::Jack:
  337. rank_name = "jack"sv;
  338. break;
  339. case Rank::Queen:
  340. rank_name = "queen"sv;
  341. break;
  342. case Rank::King:
  343. rank_name = "king"sv;
  344. break;
  345. default:
  346. break;
  347. }
  348. StringView suit_name;
  349. switch (suit) {
  350. case Suit::Diamonds:
  351. suit_name = "diamonds"sv;
  352. break;
  353. case Suit::Clubs:
  354. suit_name = "clubs"sv;
  355. break;
  356. case Suit::Hearts:
  357. suit_name = "hearts"sv;
  358. break;
  359. case Suit::Spades:
  360. suit_name = "spades"sv;
  361. break;
  362. case Suit::__Count:
  363. return;
  364. }
  365. auto front_image_path = MUST(String::formatted("/res/graphics/cards/fronts/{}/{}-{}.png", m_front_images_set_name, suit_name, rank_name));
  366. auto maybe_front_image = Gfx::Bitmap::load_from_file(front_image_path);
  367. if (maybe_front_image.is_error()) {
  368. dbgln("Failed to load `{}`: {}", front_image_path, maybe_front_image.error());
  369. return;
  370. }
  371. auto front_image = maybe_front_image.release_value();
  372. painter.blit({ (bitmap.width() - front_image->width()) / 2, (bitmap.height() - front_image->height()) / 2 }, front_image, front_image->rect());
  373. }
  374. }
  375. }
  376. void CardPainter::paint_card_back(Gfx::Bitmap& bitmap)
  377. {
  378. Gfx::Painter painter { bitmap };
  379. auto paint_rect = bitmap.rect();
  380. painter.clear_rect(paint_rect, Gfx::Color::Transparent);
  381. painter.fill_rect_with_rounded_corners(paint_rect, Color::Black, Card::card_radius);
  382. auto inner_paint_rect = paint_rect.shrunken(2, 2);
  383. painter.fill_rect_with_rounded_corners(inner_paint_rect, Color::White, Card::card_radius - 1);
  384. auto image = Gfx::Bitmap::load_from_file(m_background_image_path).release_value_but_fixme_should_propagate_errors();
  385. painter.blit({ (bitmap.width() - image->width()) / 2, (bitmap.height() - image->height()) / 2 }, image, image->rect());
  386. }
  387. void CardPainter::paint_inverted_card(Gfx::Bitmap& bitmap, Gfx::Bitmap const& source_to_invert)
  388. {
  389. Gfx::Painter painter { bitmap };
  390. painter.clear_rect(bitmap.rect(), Gfx::Color::Transparent);
  391. painter.blit_filtered(Gfx::IntPoint {}, source_to_invert, source_to_invert.rect(), [&](Color color) {
  392. return color.inverted();
  393. });
  394. }
  395. void CardPainter::paint_highlighted_card(Gfx::Bitmap& bitmap, Gfx::Bitmap const& source_to_highlight)
  396. {
  397. Gfx::Painter painter { bitmap };
  398. auto paint_rect = source_to_highlight.rect();
  399. auto background_complement = m_background_color.xored(Color::White);
  400. painter.fill_rect_with_rounded_corners(paint_rect, Color::Black, Card::card_radius);
  401. paint_rect.shrink(2, 2);
  402. painter.fill_rect_with_rounded_corners(paint_rect, background_complement, Card::card_radius - 1);
  403. paint_rect.shrink(4, 4);
  404. painter.fill_rect_with_rounded_corners(paint_rect, Color::White, Card::card_radius - 1);
  405. painter.blit({ 4, 4 }, source_to_highlight, source_to_highlight.rect().shrunken(8, 8));
  406. }
  407. }