-
Notifications
You must be signed in to change notification settings - Fork 6
/
mandelbrot_fractal.rb
424 lines (364 loc) · 12.9 KB
/
mandelbrot_fractal.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# Copyright (c) 2007-2024 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'glimmer-dsl-swt'
require 'complex'
require 'concurrent/executor/fixed_thread_pool'
require 'concurrent/utility/processor_counter'
require 'concurrent/array'
# Mandelbrot multi-threaded implementation leveraging all processor cores.
class Mandelbrot
DEFAULT_STEP = 0.0030
Y_START = -1.0
Y_END = 1.0
X_START = -2.0
X_END = 0.5
PROGRESS_MAX = 40
class << self
attr_accessor :progress, :work_in_progress
attr_writer :processor_count
def for(max_iterations:, zoom:, background: false)
key = [max_iterations, zoom]
creation_mutex.synchronize do
unless flyweight_mandelbrots.keys.include?(key)
flyweight_mandelbrots[key] = new(max_iterations: max_iterations, zoom: zoom, background: background)
end
end
flyweight_mandelbrots[key].background = background
flyweight_mandelbrots[key]
end
def flyweight_mandelbrots
@flyweight_mandelbrots ||= {}
end
def creation_mutex
@creation_mutex ||= Mutex.new
end
def processor_count
@processor_count ||= Concurrent.processor_count
end
end
attr_accessor :max_iterations, :background
attr_reader :zoom, :points_calculated
alias points_calculated? points_calculated
# max_iterations is the maximum number of Mandelbrot calculation iterations
# zoom is how much zoom there is on the Mandelbrot points from the default view of zoom 1
# background indicates whether to do calculation in the background for caching purposes,
# thus utilizing less CPU cores to avoid disrupting user experience
def initialize(max_iterations:, zoom: 1.0, background: false)
@max_iterations = max_iterations
@zoom = zoom
end
def step
DEFAULT_STEP / zoom
end
def y_array
@y_array ||= Y_START.step(Y_END, step).to_a
end
def x_array
@x_array ||= X_START.step(X_END, step).to_a
end
def height
y_array.size
end
def width
x_array.size
end
def points
@points ||= calculate_points
end
def calculate_points
puts "Background calculation activated at zoom #{zoom}" if @background
if @points_calculated
puts "Points calculated already. Returning previously calculated points..."
return @points
end
@thread_pool = Concurrent::FixedThreadPool.new(Mandelbrot.processor_count, fallback_policy: :discard)
@points = Concurrent::Array.new(height)
Mandelbrot.work_in_progress = "Calculating Mandelbrot Points for Zoom #{zoom}x"
Mandelbrot.progress = 0
point_index = 0
point_count = width*height
height.times do |y|
@points[y] ||= Concurrent::Array.new(width)
width.times do |x|
@thread_pool.post do
@points[y][x] = calculate(x_array[x], y_array[y]).last
point_index += 1
Mandelbrot.progress += 1 if (point_index.to_f / point_count.to_f)*PROGRESS_MAX >= Mandelbrot.progress
end
end
end
@thread_pool.shutdown
@thread_pool.wait_for_termination
Mandelbrot.progress = PROGRESS_MAX
@points_calculated = true
@points
end
# Calculates a Mandelbrot point, borrowing some open-source code from:
# https://github.com/gotbadger/ruby-mandelbrot
def calculate(x,y)
base_case = [Complex(x,y), 0]
Array.new(max_iterations, base_case).inject(base_case) do |prev ,base|
z, itr = prev
c, _ = base
val = z*z + c
itr += 1 unless val.abs < 2
[val, itr]
end
end
end
class MandelbrotFractal
include Glimmer::UI::CustomShell
COMMAND = OS.mac? ? :command : :ctrl
attr_accessor :mandelbrot_shell_title
option :zoom, default: 1.0
before_body do
Display.app_name = 'Mandelbrot Fractal'
# pre-calculate mandelbrot image
@mandelbrot_image = build_mandelbrot_image
end
after_body do
observe(Mandelbrot, :work_in_progress) do
update_mandelbrot_shell_title!
end
observe(Mandelbrot, :zoom) do
update_mandelbrot_shell_title!
end
# pre-calculate zoomed mandelbrot images even before the user zooms in
puts 'Starting background calculation thread...'
@thread = Thread.new do
future_zoom = 1.5
loop do
puts "Creating mandelbrot for background calculation at zoom: #{future_zoom}"
the_mandelbrot = Mandelbrot.for(max_iterations: color_palette.size - 1, zoom: future_zoom, background: true)
pixels = the_mandelbrot.calculate_points
build_mandelbrot_image(mandelbrot_zoom: future_zoom)
@canvas.cursor = :cross unless @canvas.disposed?
future_zoom += 0.5
end
end
end
body {
shell(:no_resize) {
grid_layout
text <= [self, :mandelbrot_shell_title]
minimum_size mandelbrot.width + 29, mandelbrot.height + 77
image @mandelbrot_image
on_shell_closed do
@thread.kill # should not be dangerous in this case
puts "Mandelbrot background calculation stopped!"
end
progress_bar {
layout_data :fill, :center, true, false
minimum 0
maximum Mandelbrot::PROGRESS_MAX
selection <= [Mandelbrot, :progress]
}
@scrolled_composite = scrolled_composite {
layout_data :fill, :fill, true, true
@canvas = canvas {
image @mandelbrot_image
cursor :no
on_mouse_down do
@drag_detected = false
@canvas.cursor = :hand
end
on_drag_detected do |drag_detect_event|
@drag_detected = true
@drag_start_x = drag_detect_event.x
@drag_start_y = drag_detect_event.y
end
on_mouse_move do |mouse_event|
if @drag_detected
origin = @scrolled_composite.origin
new_x = origin.x - (mouse_event.x - @drag_start_x)
new_y = origin.y - (mouse_event.y - @drag_start_y)
@scrolled_composite.set_origin(new_x, new_y)
end
end
on_mouse_up do |mouse_event|
if !@drag_detected
origin = @scrolled_composite.origin
@location_x = mouse_event.x
@location_y = mouse_event.y
if mouse_event.button == 1
zoom_in
elsif mouse_event.button > 2
zoom_out
end
end
@canvas.cursor = can_zoom_in? ? :cross : :no
@drag_detected = false
end
}
}
menu_bar {
menu {
text '&View'
menu_item {
text 'Zoom &In'
accelerator COMMAND, '+'
on_widget_selected do
zoom_in
end
}
menu_item {
text 'Zoom &Out'
accelerator COMMAND, '-'
on_widget_selected do
zoom_out
end
}
menu_item {
text '&Reset Zoom'
accelerator COMMAND, '0'
on_widget_selected do
perform_zoom(mandelbrot_zoom: 1.0)
end
}
}
menu {
text '&Cores'
Concurrent.processor_count.times do |n|
processor_number = n + 1
menu_item(:radio) {
text "&#{processor_number}"
case processor_number
when 0..9
accelerator COMMAND, processor_number.to_s
when 10..19
accelerator COMMAND, :shift, (processor_number - 10).to_s
when 20..29
accelerator COMMAND, :alt, (processor_number - 20).to_s
end
selection true if processor_number == Concurrent.processor_count
on_widget_selected do
Mandelbrot.processor_count = processor_number
end
}
end
}
menu {
text '&Help'
menu_item {
text '&Instructions'
accelerator COMMAND, :shift, :i
on_widget_selected do
display_help_instructions
end
}
}
}
}
}
def update_mandelbrot_shell_title!
new_title = "Mandelbrot Fractal - Zoom #{zoom}x (Calculated Max: #{flyweight_mandelbrot_images.keys.max}x)"
new_title += " - #{Mandelbrot.work_in_progress}" if Mandelbrot.work_in_progress
self.mandelbrot_shell_title = new_title
end
def build_mandelbrot_image(mandelbrot_zoom: nil)
mandelbrot_zoom ||= zoom
unless flyweight_mandelbrot_images.keys.include?(mandelbrot_zoom)
the_mandelbrot = mandelbrot(mandelbrot_zoom: mandelbrot_zoom)
width = the_mandelbrot.width
height = the_mandelbrot.height
pixels = the_mandelbrot.points
Mandelbrot.work_in_progress = "Consuming Points To Build Image for Zoom #{mandelbrot_zoom}x"
Mandelbrot.progress = Mandelbrot::PROGRESS_MAX + 1
point_index = 0
point_count = width*height
# invoke as a top-level parentless keyword to avoid nesting under any widget
new_mandelbrot_image = image(width, height, top_level: true) { |x, y|
point_index += 1
Mandelbrot.progress -= 1 if (Mandelbrot::PROGRESS_MAX - (point_index.to_f / point_count.to_f)*Mandelbrot::PROGRESS_MAX) < Mandelbrot.progress
pixel_color_index = pixels[y][x]
color_palette[pixel_color_index]
}
Mandelbrot.progress = 0
flyweight_mandelbrot_images[mandelbrot_zoom] = new_mandelbrot_image
update_mandelbrot_shell_title!
end
flyweight_mandelbrot_images[mandelbrot_zoom]
end
def flyweight_mandelbrot_images
@flyweight_mandelbrot_images ||= {}
end
def mandelbrot(mandelbrot_zoom: nil)
mandelbrot_zoom ||= zoom
Mandelbrot.for(max_iterations: color_palette.size - 1, zoom: mandelbrot_zoom)
end
def color_palette
if @color_palette.nil?
@color_palette = [[0, 0, 0]] + 40.times.map { |i| [255 - i*5, 255 - i*5, 55 + i*5] }
@color_palette = @color_palette.map { |color_data| rgb(*color_data).swt_color }
end
@color_palette
end
def zoom_in
if can_zoom_in?
perform_zoom(zoom_delta: 0.5)
@canvas.cursor = can_zoom_in? ? :cross : :no
end
end
def can_zoom_in?
flyweight_mandelbrot_images.keys.include?(zoom + 0.5)
end
def zoom_out
perform_zoom(zoom_delta: -0.5)
end
def perform_zoom(zoom_delta: 0, mandelbrot_zoom: nil)
mandelbrot_zoom ||= self.zoom + zoom_delta
@canvas.cursor = :wait
last_zoom = self.zoom
self.zoom = [mandelbrot_zoom, 1.0].max
@canvas.clear_shapes(dispose_images: false)
@mandelbrot_image = build_mandelbrot_image
body_root.content {
image @mandelbrot_image
}
@canvas.content {
image @mandelbrot_image
}
@canvas.set_size @mandelbrot_image.bounds.width, @mandelbrot_image.bounds.height
@scrolled_composite.set_min_size(Point.new(@mandelbrot_image.bounds.width, @mandelbrot_image.bounds.height))
if @location_x && @location_y
# center on mouse click location
factor = (zoom / last_zoom)
@scrolled_composite.set_origin(factor*@location_x - @scrolled_composite.client_area.width/2.0, factor*@location_y - @scrolled_composite.client_area.height/2.0)
@location_x = @location_y = nil
end
update_mandelbrot_shell_title!
@canvas.cursor = :cross
end
def display_help_instructions
message_box(body_root) {
text 'Mandelbrot Fractal - Help'
message <<~MULTI_LINE_STRING
The Mandelbrot Fractal precalculates zoomed renderings in the background. Wait if you hit a zoom level that is not calculated yet.
Left-click to zoom in.
Right-click to zoom out.
Scroll or drag to pan.
Adjust cores to get a more responsive interaction.
Enjoy!
MULTI_LINE_STRING
}.open
end
end
MandelbrotFractal.launch