CardPainter.cpp 17 KB

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