Mandelbrot.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include <LibCore/System.h>
  8. #include <LibGUI/Action.h>
  9. #include <LibGUI/Application.h>
  10. #include <LibGUI/FilePicker.h>
  11. #include <LibGUI/Icon.h>
  12. #include <LibGUI/Menu.h>
  13. #include <LibGUI/Menubar.h>
  14. #include <LibGUI/MessageBox.h>
  15. #include <LibGUI/Painter.h>
  16. #include <LibGUI/Widget.h>
  17. #include <LibGUI/Window.h>
  18. #include <LibGfx/Bitmap.h>
  19. #include <LibGfx/ImageFormats/BMPWriter.h>
  20. #include <LibGfx/ImageFormats/PNGWriter.h>
  21. #include <LibGfx/ImageFormats/QOIWriter.h>
  22. #include <LibMain/Main.h>
  23. #include <unistd.h>
  24. class MandelbrotSet {
  25. public:
  26. MandelbrotSet()
  27. {
  28. set_view();
  29. }
  30. void reset()
  31. {
  32. set_view();
  33. correct_aspect();
  34. calculate();
  35. }
  36. void resize(Gfx::IntSize size)
  37. {
  38. m_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, size).release_value_but_fixme_should_propagate_errors();
  39. correct_aspect();
  40. calculate();
  41. }
  42. void zoom(Gfx::IntRect const& rect)
  43. {
  44. set_view(
  45. rect.left() * (m_x_end - m_x_start) / m_bitmap->width() + m_x_start,
  46. (rect.right() - 1) * (m_x_end - m_x_start) / m_bitmap->width() + m_x_start,
  47. rect.top() * (m_y_end - m_y_start) / m_bitmap->height() + m_y_start,
  48. (rect.bottom() - 1) * (m_y_end - m_y_start) / m_bitmap->height() + m_y_start);
  49. correct_aspect();
  50. calculate();
  51. }
  52. void pan_by(Gfx::IntPoint delta)
  53. {
  54. auto relative_width_pixel = (m_x_end - m_x_start) / m_bitmap->width();
  55. auto relative_height_pixel = (m_y_end - m_y_start) / m_bitmap->height();
  56. set_view(
  57. m_x_start - delta.x() * relative_width_pixel,
  58. m_x_end - delta.x() * relative_width_pixel,
  59. m_y_start - delta.y() * relative_height_pixel,
  60. m_y_end - delta.y() * relative_height_pixel);
  61. Gfx::IntRect horizontal_missing, vertical_missing;
  62. if (delta.y() >= 0) {
  63. horizontal_missing = { 0, 0, m_bitmap->width(), delta.y() };
  64. } else {
  65. horizontal_missing = { 0, m_bitmap->height() + delta.y(), m_bitmap->width(), -delta.y() };
  66. }
  67. if (delta.x() >= 0) {
  68. vertical_missing = { 0, 0, delta.x(), m_bitmap->height() };
  69. } else {
  70. vertical_missing = { m_bitmap->width() + delta.x(), 0, -delta.x(), m_bitmap->height() };
  71. }
  72. move_contents_by(delta);
  73. calculate_rect(horizontal_missing);
  74. calculate_rect(vertical_missing);
  75. }
  76. double mandelbrot(double px, double py, i32 max_iterations)
  77. {
  78. // Based on https://en.wikipedia.org/wiki/Plotting_algorithms_for_the_Mandelbrot_set
  79. double const x0 = px * (m_x_end - m_x_start) / m_bitmap->width() + m_x_start;
  80. double const y0 = py * (m_y_end - m_y_start) / m_bitmap->height() + m_y_start;
  81. double x = 0;
  82. double y = 0;
  83. i32 iteration = 0;
  84. double x2 = 0;
  85. double y2 = 0;
  86. while (x2 + y2 <= 4 && iteration < max_iterations) {
  87. y = 2 * x * y + y0;
  88. x = x2 - y2 + x0;
  89. x2 = x * x;
  90. y2 = y * y;
  91. iteration++;
  92. }
  93. if (iteration == max_iterations)
  94. return iteration;
  95. auto lz = sqrt(x * x + y * y) / 2;
  96. return 1 + iteration + log(lz / log(2)) / log(2);
  97. }
  98. static double linear_interpolate(double v0, double v1, double t)
  99. {
  100. return v0 + t * (v1 - v0);
  101. }
  102. void calculate_pixel(int px, int py, int max_iterations)
  103. {
  104. auto iterations = mandelbrot(px, py, max_iterations);
  105. auto whole_iterations = floor(iterations);
  106. auto partial_iterations = fmod(iterations, 1);
  107. double hue1 = whole_iterations * 360.0 / max_iterations;
  108. if (hue1 >= 360.0)
  109. hue1 = 0.0;
  110. double hue2 = (whole_iterations + 1) * 360.0 / max_iterations;
  111. if (hue2 >= 360.0)
  112. hue2 = 0.0;
  113. double hue = linear_interpolate(hue1, hue2, partial_iterations);
  114. double saturation = 1.0;
  115. double value = iterations < max_iterations ? 1.0 : 0;
  116. m_bitmap->set_pixel(px, py, Color::from_hsv(hue, saturation, value));
  117. }
  118. void calculate(int max_iterations = 100)
  119. {
  120. VERIFY(m_bitmap);
  121. calculate_rect(m_bitmap->rect(), max_iterations);
  122. }
  123. void calculate_rect(Gfx::IntRect const& rect, int max_iterations = 100)
  124. {
  125. VERIFY(m_bitmap);
  126. if (rect.is_empty())
  127. return;
  128. for (int py = rect.top(); py < rect.bottom(); py++)
  129. for (int px = rect.left(); px < rect.right(); px++)
  130. calculate_pixel(px, py, max_iterations);
  131. }
  132. Gfx::Bitmap const& bitmap() const
  133. {
  134. return *m_bitmap;
  135. }
  136. private:
  137. double m_x_start { 0 };
  138. double m_x_end { 0 };
  139. double m_y_start { 0 };
  140. double m_y_end { 0 };
  141. RefPtr<Gfx::Bitmap> m_bitmap;
  142. void set_view(double x_start = -2.5, double x_end = 1.0, double y_start = -1.75, double y_end = 1.75)
  143. {
  144. m_x_start = x_start;
  145. m_x_end = x_end;
  146. m_y_start = y_start;
  147. m_y_end = y_end;
  148. }
  149. void correct_aspect()
  150. {
  151. auto y_mid = m_y_start + (m_y_end - m_y_start) / 2;
  152. auto aspect_corrected_y_length = (m_x_end - m_x_start) * m_bitmap->height() / m_bitmap->width();
  153. m_y_start = y_mid - aspect_corrected_y_length / 2;
  154. m_y_end = y_mid + aspect_corrected_y_length / 2;
  155. }
  156. void move_contents_by(Gfx::IntPoint delta)
  157. {
  158. // If we're moving down we paint upwards, else we paint downwards, to
  159. // avoid overwriting.
  160. if (delta.y() >= 0) {
  161. for (int row = m_bitmap->physical_height() - 1; row >= delta.y(); row--)
  162. move_row(row - delta.y(), row, delta.x());
  163. } else {
  164. for (int row = 0; row < m_bitmap->physical_height() + delta.y(); row++)
  165. move_row(row - delta.y(), row, delta.x());
  166. }
  167. }
  168. void move_row(int from, int to, int x_delta)
  169. {
  170. // If we're moving right we paint RTL, else we paint LTR, to avoid
  171. // overwriting.
  172. if (x_delta >= 0) {
  173. for (int column = m_bitmap->physical_width() - 1; column >= x_delta; column--) {
  174. m_bitmap->set_pixel(column, to, m_bitmap->get_pixel(column - x_delta, from));
  175. }
  176. } else {
  177. for (int column = 0; column < m_bitmap->physical_width() + x_delta; column++) {
  178. m_bitmap->set_pixel(column, to, m_bitmap->get_pixel(column - x_delta, from));
  179. }
  180. }
  181. }
  182. };
  183. enum class ImageType {
  184. BMP,
  185. PNG,
  186. QOI
  187. };
  188. class Mandelbrot : public GUI::Frame {
  189. C_OBJECT(Mandelbrot)
  190. ErrorOr<void> export_image(DeprecatedString const& export_path, ImageType image_type);
  191. enum class Zoom {
  192. In,
  193. Out,
  194. };
  195. void zoom(Zoom in_out, Gfx::IntPoint center);
  196. void reset();
  197. private:
  198. Mandelbrot() = default;
  199. virtual void paint_event(GUI::PaintEvent&) override;
  200. virtual void mousedown_event(GUI::MouseEvent& event) override;
  201. virtual void mousemove_event(GUI::MouseEvent& event) override;
  202. virtual void mouseup_event(GUI::MouseEvent& event) override;
  203. virtual void mousewheel_event(GUI::MouseEvent& event) override;
  204. virtual void resize_event(GUI::ResizeEvent& event) override;
  205. bool m_dragging { false };
  206. Gfx::IntPoint m_selection_start;
  207. Gfx::IntPoint m_selection_end;
  208. bool m_panning { false };
  209. Gfx::IntPoint m_last_pan_position;
  210. MandelbrotSet m_set;
  211. };
  212. void Mandelbrot::zoom(Zoom in_out, Gfx::IntPoint center)
  213. {
  214. static constexpr double zoom_in_multiplier = 0.8;
  215. static constexpr double zoom_out_multiplier = 1.25;
  216. bool zooming_in = in_out == Zoom::In;
  217. double multiplier = zooming_in ? zoom_in_multiplier : zoom_out_multiplier;
  218. auto relative_rect = this->relative_rect();
  219. Gfx::IntRect zoomed_rect = relative_rect;
  220. zoomed_rect.set_width(zoomed_rect.width() * multiplier);
  221. zoomed_rect.set_height(zoomed_rect.height() * multiplier);
  222. auto leftover_width = abs(relative_rect.width() - zoomed_rect.width());
  223. auto leftover_height = abs(relative_rect.height() - zoomed_rect.height());
  224. double cursor_x_percentage = static_cast<double>(center.x()) / relative_rect.width();
  225. double cursor_y_percentage = static_cast<double>(center.y()) / relative_rect.height();
  226. zoomed_rect.set_x((zooming_in ? 1 : -1) * leftover_width * cursor_x_percentage);
  227. zoomed_rect.set_y((zooming_in ? 1 : -1) * leftover_height * cursor_y_percentage);
  228. m_set.zoom(zoomed_rect);
  229. update();
  230. }
  231. void Mandelbrot::reset()
  232. {
  233. m_set.reset();
  234. update();
  235. }
  236. void Mandelbrot::paint_event(GUI::PaintEvent& event)
  237. {
  238. Frame::paint_event(event);
  239. GUI::Painter painter(*this);
  240. painter.add_clip_rect(frame_inner_rect());
  241. painter.add_clip_rect(event.rect());
  242. painter.draw_scaled_bitmap(rect(), m_set.bitmap(), m_set.bitmap().rect());
  243. if (m_dragging)
  244. painter.draw_rect(Gfx::IntRect::from_two_points(m_selection_start, m_selection_end), Color::Blue);
  245. }
  246. void Mandelbrot::mousedown_event(GUI::MouseEvent& event)
  247. {
  248. if (event.button() == GUI::MouseButton::Primary) {
  249. if (!m_dragging) {
  250. m_selection_start = event.position();
  251. m_selection_end = event.position();
  252. m_dragging = true;
  253. update();
  254. }
  255. } else if (event.button() == GUI::MouseButton::Middle) {
  256. if (!m_panning) {
  257. m_last_pan_position = event.position();
  258. m_panning = true;
  259. update();
  260. }
  261. }
  262. return GUI::Widget::mousedown_event(event);
  263. }
  264. void Mandelbrot::mousemove_event(GUI::MouseEvent& event)
  265. {
  266. if (m_dragging) {
  267. // Maintain aspect ratio
  268. int selection_width = event.position().x() - m_selection_start.x();
  269. int selection_height = event.position().y() - m_selection_start.y();
  270. int aspect_corrected_selection_width = selection_height * width() / height();
  271. int aspect_corrected_selection_height = selection_width * height() / width();
  272. if (selection_width * aspect_corrected_selection_height > aspect_corrected_selection_width * selection_height)
  273. m_selection_end = { event.position().x(), m_selection_start.y() + abs(aspect_corrected_selection_height) * ((selection_height < 0) ? -1 : 1) };
  274. else
  275. m_selection_end = { m_selection_start.x() + abs(aspect_corrected_selection_width) * ((selection_width < 0) ? -1 : 1), event.position().y() };
  276. update();
  277. }
  278. if (m_panning) {
  279. m_set.pan_by(event.position() - m_last_pan_position);
  280. m_last_pan_position = event.position();
  281. update();
  282. }
  283. return GUI::Widget::mousemove_event(event);
  284. }
  285. void Mandelbrot::mouseup_event(GUI::MouseEvent& event)
  286. {
  287. if (event.button() == GUI::MouseButton::Primary) {
  288. auto selection = Gfx::IntRect::from_two_points(m_selection_start, m_selection_end);
  289. if (selection.width() > 0 && selection.height() > 0)
  290. m_set.zoom(selection);
  291. m_dragging = false;
  292. update();
  293. } else if (event.button() == GUI::MouseButton::Middle) {
  294. m_panning = false;
  295. update();
  296. } else if (event.button() == GUI::MouseButton::Secondary) {
  297. reset();
  298. }
  299. return GUI::Widget::mouseup_event(event);
  300. }
  301. void Mandelbrot::mousewheel_event(GUI::MouseEvent& event)
  302. {
  303. zoom(event.wheel_delta_y() < 0 ? Zoom::In : Zoom::Out, event.position());
  304. }
  305. void Mandelbrot::resize_event(GUI::ResizeEvent& event)
  306. {
  307. m_set.resize(event.size());
  308. }
  309. ErrorOr<void> Mandelbrot::export_image(DeprecatedString const& export_path, ImageType image_type)
  310. {
  311. m_set.resize(Gfx::IntSize { 1920, 1080 });
  312. ByteBuffer encoded_data;
  313. switch (image_type) {
  314. case ImageType::BMP:
  315. encoded_data = TRY(Gfx::BMPWriter::encode(m_set.bitmap()));
  316. break;
  317. case ImageType::PNG:
  318. encoded_data = TRY(Gfx::PNGWriter::encode(m_set.bitmap()));
  319. break;
  320. case ImageType::QOI:
  321. encoded_data = TRY(Gfx::QOIWriter::encode(m_set.bitmap()));
  322. break;
  323. default:
  324. VERIFY_NOT_REACHED();
  325. }
  326. m_set.resize(size());
  327. auto file = fopen(export_path.characters(), "wb");
  328. if (!file) {
  329. GUI::MessageBox::show(window(), DeprecatedString::formatted("Could not open '{}' for writing.", export_path), "Mandelbrot"sv, GUI::MessageBox::Type::Error);
  330. return {};
  331. }
  332. fwrite(encoded_data.data(), 1, encoded_data.size(), file);
  333. fclose(file);
  334. return {};
  335. }
  336. ErrorOr<int> serenity_main(Main::Arguments arguments)
  337. {
  338. auto app = TRY(GUI::Application::create(arguments));
  339. TRY(Core::System::pledge("stdio thread recvfd sendfd rpath unix wpath cpath"));
  340. #if 0
  341. TRY(Core::System::unveil("/res", "r"));
  342. TRY(unveil(nullptr, nullptr));
  343. #endif
  344. auto window = TRY(GUI::Window::try_create());
  345. window->set_double_buffering_enabled(false);
  346. window->set_title("Mandelbrot");
  347. window->set_obey_widget_min_size(false);
  348. window->set_minimum_size(320, 240);
  349. window->resize(window->minimum_size() * 2);
  350. auto mandelbrot = TRY(window->set_main_widget<Mandelbrot>());
  351. auto file_menu = window->add_menu("&File"_string);
  352. auto export_submenu = file_menu->add_submenu("&Export"_string);
  353. export_submenu->add_action(GUI::Action::create("As &BMP...",
  354. [&](GUI::Action&) {
  355. Optional<DeprecatedString> export_path = GUI::FilePicker::get_save_filepath(window, "untitled", "bmp");
  356. if (!export_path.has_value())
  357. return;
  358. if (auto result = mandelbrot->export_image(export_path.value(), ImageType::BMP); result.is_error())
  359. GUI::MessageBox::show_error(window, DeprecatedString::formatted("{}", result.error()));
  360. }));
  361. export_submenu->add_action(GUI::Action::create("As &PNG...", { Mod_Ctrl | Mod_Shift, Key_S },
  362. [&](GUI::Action&) {
  363. Optional<DeprecatedString> export_path = GUI::FilePicker::get_save_filepath(window, "untitled", "png");
  364. if (!export_path.has_value())
  365. return;
  366. if (auto result = mandelbrot->export_image(export_path.value(), ImageType::PNG); result.is_error())
  367. GUI::MessageBox::show_error(window, DeprecatedString::formatted("{}", result.error()));
  368. }));
  369. export_submenu->add_action(GUI::Action::create("As &QOI...",
  370. [&](GUI::Action&) {
  371. Optional<DeprecatedString> export_path = GUI::FilePicker::get_save_filepath(window, "untitled", "qoi");
  372. if (!export_path.has_value())
  373. return;
  374. if (auto result = mandelbrot->export_image(export_path.value(), ImageType::QOI); result.is_error())
  375. GUI::MessageBox::show_error(window, DeprecatedString::formatted("{}", result.error()));
  376. }));
  377. export_submenu->set_icon(TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"sv)));
  378. file_menu->add_separator();
  379. file_menu->add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); }));
  380. auto zoom_in_action = GUI::CommonActions::make_zoom_in_action(
  381. [&](auto&) {
  382. mandelbrot->zoom(Mandelbrot::Zoom::In, mandelbrot->relative_rect().center());
  383. },
  384. window);
  385. auto reset_zoom_action = GUI::CommonActions::make_reset_zoom_action(
  386. [&](auto&) {
  387. // FIXME: Ideally, this would only reset zoom. Currently, it resets pan too.
  388. mandelbrot->reset();
  389. },
  390. window);
  391. auto zoom_out_action = GUI::CommonActions::make_zoom_out_action(
  392. [&](auto&) {
  393. mandelbrot->zoom(Mandelbrot::Zoom::Out, mandelbrot->relative_rect().center());
  394. },
  395. window);
  396. auto app_icon = GUI::Icon::default_icon("app-mandelbrot"sv);
  397. window->set_icon(app_icon.bitmap_for_size(16));
  398. auto view_menu = window->add_menu("&View"_string);
  399. view_menu->add_action(zoom_in_action);
  400. view_menu->add_action(reset_zoom_action);
  401. view_menu->add_action(zoom_out_action);
  402. auto help_menu = window->add_menu("&Help"_string);
  403. help_menu->add_action(GUI::CommonActions::make_command_palette_action(window));
  404. help_menu->add_action(GUI::CommonActions::make_about_action("Mandelbrot Demo"_string, app_icon, window));
  405. window->show();
  406. window->set_cursor(Gfx::StandardCursor::Zoom);
  407. return app->exec();
  408. }