Skip to content

Commit c78e853

Browse files
committed
Add observer pattern to Mandelbrot window
1 parent 2bb27f3 commit c78e853

File tree

10 files changed

+250
-158
lines changed

10 files changed

+250
-158
lines changed

source/config.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
#pragma once
22

3+
#include "coordinates.hpp"
4+
35
#include <cstddef>
46

57
namespace fractal {
8+
69
constexpr std::size_t WINDOW_WIDTH = 800UZ;
710
constexpr std::size_t WINDOW_HEIGHT = 600UZ;
811
constexpr std::size_t FRAME_RATE = 60UZ;
12+
13+
constexpr display_domain DISPLAY_DOMAIN{
14+
{0, 0 },
15+
{WINDOW_WIDTH - 1, WINDOW_HEIGHT - 1}
16+
};
17+
18+
constexpr complex_domain START_COMPLEX_DOMAIN{
19+
{complex_underlying{-2}, complex_underlying{-1.5}},
20+
{complex_underlying{1}, complex_underlying{1.5} }
21+
};
922
} // namespace fractal

source/graphics/basic_display.cpp

Lines changed: 20 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,16 @@
11
#include "basic_display.hpp"
22

33
#include "coordinates.hpp"
4-
#include "graphics/color_conversions.hpp"
4+
#include "ends.hpp"
55

66
#include <fmt/format.h>
77

88
#include <cmath>
99

1010
namespace fractal {
11-
void BasicDisplay::set_pixel(display_coordinate coordinate, uint16_t value)
12-
{
13-
auto tuple = number_to_rgb(value);
14-
15-
image_.setPixel(
16-
static_cast<unsigned int>(coordinate.first),
17-
static_cast<unsigned int>(coordinate.second),
18-
sf::Color(std::get<0>(tuple), std::get<1>(tuple), std::get<2>(tuple))
19-
);
20-
}
21-
22-
display_coordinate calculate_end_points(
23-
display_coordinate start, display_coordinate current,
24-
float target_aspect_ratio = 800.0f / 600.0f
25-
)
26-
{
27-
auto width = static_cast<float>(std::abs(current.first - start.first));
28-
auto height = static_cast<float>(std::abs(current.second - start.second));
29-
30-
// Adjust the dimensions to maintain the target aspect ratio
31-
if (width / height > target_aspect_ratio) {
32-
// Too wide, adjust width
33-
width = height * target_aspect_ratio;
34-
}
35-
else {
36-
// Too tall, adjust height
37-
height = width / target_aspect_ratio;
38-
}
39-
40-
auto x = static_cast<float>(std::min(current.first, start.first));
41-
auto y = static_cast<float>(std::min(current.second, start.second));
42-
43-
// Adjust the top-left corner based on new dimensions
44-
if (current.first < start.first) {
45-
x = static_cast<float>(start.first) - width;
46-
}
47-
if (current.second < start.second) {
48-
y = static_cast<float>(start.second) - height;
49-
}
50-
51-
// Return the top-left and bottom-right corners as a pair of sf::Vector2f
52-
return {x + width, y + height};
53-
}
5411

5512
void BasicDisplay::display_window()
5613
{
57-
texture_.loadFromImage(image_);
58-
sf::Sprite sprite{texture_};
5914
bool left_mouse_down{};
6015
float selection_start_x{};
6116
float selection_start_y{};
@@ -81,6 +36,12 @@ void BasicDisplay::display_window()
8136
left_mouse_down = true;
8237
selection_start_x = static_cast<float>(event.mouseButton.x);
8338
selection_start_y = static_cast<float>(event.mouseButton.y);
39+
std::for_each(
40+
observers_.begin(), observers_.end(),
41+
[&](const auto& observer) {
42+
observer->on_mouse_button_pressed(event.mouseButton);
43+
}
44+
);
8445
break;
8546
default:
8647
break;
@@ -89,24 +50,26 @@ void BasicDisplay::display_window()
8950
if (event.mouseButton.button != sf::Mouse::Left) {
9051
break;
9152
}
92-
auto ends = calculate_end_points(
93-
{selection_start_x, selection_start_y}, {mouse_x, mouse_y}
94-
);
95-
on_resize_(
96-
sf::Vector2f(
97-
std::min(mouse_x, selection_start_x),
98-
std::min(mouse_y, selection_start_y)
99-
),
100-
sf::Vector2f(ends.first, ends.second)
53+
std::for_each(
54+
observers_.begin(), observers_.end(),
55+
[&](const auto& observer) {
56+
observer->on_mouse_button_released(event.mouseButton);
57+
}
10158
);
10259
left_mouse_down = false;
103-
return;
60+
break;
10461
}
10562
}
10663

10764
window_.clear(sf::Color::Black);
10865

109-
window_.draw(sprite);
66+
for (const auto& observer : observers_) {
67+
auto opt = observer->get_drawable();
68+
if (opt) {
69+
window_.draw(opt.value());
70+
}
71+
}
72+
11073
if (left_mouse_down) {
11174
display_coordinate end_point = calculate_end_points(
11275
{selection_start_x, selection_start_y}, {mouse_x, mouse_y}

source/graphics/basic_display.hpp

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,29 @@
11
#pragma once
22

33
#include "config.hpp"
4-
#include "coordinates.hpp"
4+
#include "graphics/display_event_observer.hpp"
55

66
#include <SFML/Graphics.hpp>
77
#include <SFML/Graphics/RectangleShape.hpp>
88
#include <SFML/System/Vector2.hpp>
99

10-
#include <cstdint>
11-
12-
#include <functional>
10+
#include <memory>
1311
#include <utility>
12+
#include <vector>
1413

1514
namespace fractal {
1615
class BasicDisplay {
1716
sf::RenderWindow window_{sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), "SFML Window"};
18-
sf::Image image_;
19-
sf::Texture texture_;
20-
std::function<void(sf::Vector2f, sf::Vector2f)> on_resize_;
17+
std::vector<std::shared_ptr<DisplayEventObserver>> observers_;
2118

2219
public:
23-
explicit BasicDisplay(std::function<void(sf::Vector2f, sf::Vector2f)> on_resize
20+
explicit BasicDisplay() { window_.setFramerateLimit(FRAME_RATE); }
2421

25-
) : on_resize_(std::move(on_resize))
22+
void add_observer(std::shared_ptr<DisplayEventObserver> observer)
2623
{
27-
window_.setFramerateLimit(FRAME_RATE);
28-
image_.create(WINDOW_WIDTH, WINDOW_HEIGHT);
24+
observers_.push_back(std::move(observer));
2925
}
3026

31-
void set_pixel(display_coordinate coordinate, uint16_t value);
3227
void display_window();
3328
};
3429
} // namespace fractal

source/graphics/color_conversions.cpp

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
#include "color_conversions.hpp"
22

3+
#include <fmt/format.h>
4+
35
#include <cmath>
46

7+
#include <stdexcept>
8+
59
namespace fractal {
6-
std::tuple<uint16_t, uint16_t, uint16_t>
7-
hsv_to_rgb(float hue, float saturation, float value)
10+
color hsv_to_rgb(float hue, float saturation, float value)
811
{
912
float chroma = value * saturation;
1013
float hue_prime = hue / 60.0f;
@@ -50,16 +53,19 @@ hsv_to_rgb(float hue, float saturation, float value)
5053
float green = green_temp + match_value;
5154
float blue = blue_temp + match_value;
5255

53-
auto red_int = static_cast<uint16_t>(red * 255);
54-
auto green_int = static_cast<uint16_t>(green * 255);
55-
auto blue_int = static_cast<uint16_t>(blue * 255);
56+
auto red_int = static_cast<uint8_t>(red * 255);
57+
auto green_int = static_cast<uint8_t>(green * 255);
58+
auto blue_int = static_cast<uint8_t>(blue * 255);
5659

5760
return {red_int, green_int, blue_int};
5861
}
5962

60-
std::tuple<uint16_t, uint16_t, uint16_t> number_to_rgb(uint16_t number)
63+
color ratio_to_rgb(float ratio)
6164
{
62-
float hue = (static_cast<float>(number) / 65535.0f) * 360.0f;
65+
if (ratio < 0 || ratio > 1) {
66+
throw std::out_of_range(fmt::format("Ratio out of range: {}", ratio));
67+
}
68+
float hue = static_cast<float>(ratio) * 360.0f;
6369
float saturation = 1.0f;
6470
float value = 1.0f;
6571

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
#pragma once
22

3-
#include <cstdint>
4-
5-
#include <tuple>
3+
#include "units.hpp"
64

75
namespace fractal {
8-
std::tuple<uint16_t, uint16_t, uint16_t>
9-
hsv_to_rgb(float hue, float saturation, float value);
6+
color hsv_to_rgb(float hue, float saturation, float value);
107

11-
std::tuple<uint16_t, uint16_t, uint16_t> number_to_rgb(uint16_t number);
8+
color ratio_to_rgb(float ratio);
129
} // namespace fractal
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#pragma once
2+
3+
#include <SFML/Graphics/Drawable.hpp>
4+
#include <SFML/Window/Event.hpp>
5+
6+
#include <memory>
7+
#include <optional>
8+
9+
namespace fractal {
10+
class DisplayEventObserver {
11+
public:
12+
virtual void on_mouse_moved(const sf::Event::MouseMoveEvent&) {}
13+
14+
virtual void on_mouse_button_pressed(const sf::Event::MouseButtonEvent&) {}
15+
16+
virtual void on_mouse_button_released(const sf::Event::MouseButtonEvent&) {}
17+
18+
virtual std::optional<std::reference_wrapper<sf::Drawable>> get_drawable() = 0;
19+
20+
DisplayEventObserver() = default;
21+
DisplayEventObserver(DisplayEventObserver&&) = default;
22+
DisplayEventObserver(const DisplayEventObserver&) = default;
23+
DisplayEventObserver& operator=(const DisplayEventObserver&) = default;
24+
DisplayEventObserver& operator=(DisplayEventObserver&&) = default;
25+
virtual ~DisplayEventObserver() = default;
26+
};
27+
} // namespace fractal

source/graphics/ends.hpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#pragma once
2+
3+
#include "coordinates.hpp"
4+
5+
namespace fractal {
6+
inline display_coordinate calculate_end_points(
7+
display_coordinate start, display_coordinate current,
8+
float target_aspect_ratio = 800.0f / 600.0f
9+
)
10+
{
11+
auto width = static_cast<float>(std::abs(current.first - start.first));
12+
auto height = static_cast<float>(std::abs(current.second - start.second));
13+
14+
// Adjust the dimensions to maintain the target aspect ratio
15+
if (width / height > target_aspect_ratio) {
16+
// Too wide, adjust width
17+
width = height * target_aspect_ratio;
18+
}
19+
else {
20+
// Too tall, adjust height
21+
height = width / target_aspect_ratio;
22+
}
23+
24+
auto x = static_cast<float>(std::min(current.first, start.first));
25+
auto y = static_cast<float>(std::min(current.second, start.second));
26+
27+
// Adjust the top-left corner based on new dimensions
28+
if (current.first < start.first) {
29+
x = static_cast<float>(start.first) - width;
30+
}
31+
if (current.second < start.second) {
32+
y = static_cast<float>(start.second) - height;
33+
}
34+
35+
// Return the top-left and bottom-right corners as a pair of sf::Vector2f
36+
return {x + width, y + height};
37+
}
38+
} // namespace fractal

source/main.cpp

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,17 @@
1-
#include "config.hpp"
2-
#include "coordinates.hpp"
31
#include "graphics/basic_display.hpp"
4-
#include "graphics/display_to_complex.hpp"
5-
#include "units.hpp"
2+
#include "mandelbrot.hpp"
63

74
#include <argparse/argparse.hpp>
85

9-
#include <complex>
10-
11-
#include <algorithm>
12-
#include <execution>
13-
#include <limits>
14-
156
namespace fractal {
167

17-
const complex_underlying DIVERGENCE_NORM = 4;
18-
const display_domain DISPLAY_DOMAIN{
19-
{0, 0 },
20-
{WINDOW_WIDTH - 1, WINDOW_HEIGHT - 1}
21-
};
22-
const complex_domain COMPLEX_DOMAIN{
23-
{complex_underlying{-2}, complex_underlying{-1.5}},
24-
{complex_underlying{1}, complex_underlying{1.5} }
25-
};
26-
constexpr std::size_t MAX_ITERATIONS = 512;
27-
28-
// https://en.wikipedia.org/wiki/Mandelbrot_set#Formal_definition
29-
std::complex<complex_underlying>
30-
step(std::complex<complex_underlying> z_n, std::complex<complex_underlying> constant)
31-
{
32-
return z_n * z_n + constant;
33-
}
34-
35-
std::size_t compute_iterations(
36-
std::complex<complex_underlying> z_0, std::complex<complex_underlying> constant,
37-
std::size_t max_iters
38-
)
39-
{
40-
std::size_t iterations = 0;
41-
std::complex<complex_underlying> z_n = z_0;
42-
43-
while (iterations < max_iters && std::norm(z_n) < DIVERGENCE_NORM) {
44-
z_n = step(z_n, constant);
45-
++iterations;
46-
}
47-
48-
return iterations;
49-
}
50-
518
void display_mandelbrot()
529
{
53-
complex_domain domain = COMPLEX_DOMAIN;
54-
DisplayToComplexCoordinates to_complex{DISPLAY_DOMAIN.end_coordinate, domain};
55-
56-
auto on_resize = [&](sf::Vector2f first, sf::Vector2f second) {
57-
complex_coordinate top = to_complex.to_complex_projection({first.x, first.y});
58-
complex_coordinate bottom =
59-
to_complex.to_complex_projection({second.x, second.y});
60-
to_complex = {
61-
DISPLAY_DOMAIN.end_coordinate, {top, bottom}
62-
};
63-
};
64-
BasicDisplay display(on_resize);
10+
auto t = std::make_shared<Mandelbrot>();
11+
BasicDisplay display;
12+
display.add_observer(t);
6513

6614
while (true) {
67-
auto process_coordinate = [&](const display_coordinate& coord) {
68-
auto complex_coord = to_complex.to_complex_projection(coord);
69-
70-
// Compute the number of iterations
71-
auto iterations = compute_iterations({0, 0}, complex_coord, MAX_ITERATIONS);
72-
73-
display.set_pixel(
74-
coord, static_cast<uint16_t>(
75-
(static_cast<float>(iterations)
76-
/ static_cast<float>(MAX_ITERATIONS))
77-
* std::numeric_limits<uint16_t>::max()
78-
)
79-
);
80-
};
81-
82-
std::for_each(
83-
std::execution::par_unseq, DISPLAY_DOMAIN.begin(), DISPLAY_DOMAIN.end(),
84-
process_coordinate
85-
);
86-
8715
display.display_window();
8816
}
8917
}

0 commit comments

Comments
 (0)