Mandelbrot
Mix.install([
{:nx, "~> 0.4.1"},
{:stb_image, "~> 0.6.0"},
{:kino, "~> 0.11.3"},
{:tint, "~> 1.1"},
{:flow, "~> 1.2"}
])
Introduction
Inspiration:
Setup
Mandelbrot scale (which Mandelbrot window to calculate):
west = -2.00
east = 0.6
north = 1.2
south = -1.2
{west, east, north, south}
{mb_width, mb_height} = {east - west, north - south}
Canvas size:
{canvas_width, canvas_height} = {trunc(mb_width * 300), trunc(mb_height * 300)}
Iteration limit:
max_iterations = 360
palette_input = Kino.Input.select("Palette for coloring:", hue: "Hue", greyscale: "Greyscale")
method_input =
Kino.Input.select("Calculation method:",
flow: "Concurrently with 'flow' (9 min on my laptop)",
serial: "Serial (33 min on my laptop)"
)
Palette
Greyscale palette:
palette_greyscale = fn value, max ->
index = trunc(255 * value / max)
[index, index, index]
end
Hue-based palette
palette_hue = fn
max, max ->
[0, 0, 0]
value, max ->
Tint.HSV.new(359.9999 * value / max, 1, 1)
|> Tint.to_rgb()
|> Tint.RGB.to_tuple()
|> Tuple.to_list()
end
Pick selected palette:
palette =
case Kino.Input.read(palette_input) do
:greyscale -> palette_greyscale
:hue -> palette_hue
end
Mandelbrot Function
mandelbrot = fn canvas_x, canvas_y, maxi ->
x0 = west + canvas_x / canvas_width * mb_width
y0 = south + canvas_y / canvas_height * mb_height
recurser = fn recurser, x0, y0, x, y, i, maxi ->
case {x * x + y * y, i < maxi} do
{value, true} when value <= 4 ->
xtemp = x * x - y * y + x0
y = 2 * x * y + y0
x = xtemp
recurser.(recurser, x0, y0, x, y, i + 1, maxi)
_ ->
i
end
end
recurser.(recurser, x0, y0, 0.0, 0.0, 0, maxi)
end
Methods
method_serial = fn canvas_height, canvas_width, max_iterations ->
0..(canvas_height - 1)
|> Enum.map(fn y ->
0..(canvas_width - 1)
|> Enum.map(fn x ->
mandelbrot.(x, y, max_iterations)
|> palette.(max_iterations)
end)
end)
end
method_flow = fn canvas_height, canvas_width, max_iterations ->
0..(canvas_height - 1)
|> Flow.from_enumerable(min_demand: 1, max_demand: 20)
|> Flow.map(fn y ->
payload =
0..(canvas_width - 1)
|> Enum.map(fn x ->
mandelbrot.(x, y, max_iterations)
|> palette.(max_iterations)
end)
{y, payload}
end)
|> Enum.to_list()
|> Enum.sort(fn {i1, _}, {i2, _} -> i1 >= i2 end)
|> Enum.map(fn {_, p} -> p end)
end
method =
case Kino.Input.read(method_input) do
:serial -> method_serial
:flow -> method_flow
end
Calculate
This is the part that takes time:
data = method.(canvas_height, canvas_width, max_iterations)
Rendering
image =
Nx.tensor(
data,
type: {:u, 8},
names: [:height, :width, :channels]
)
StbImage.from_nx(image)
|> StbImage.to_binary(:png)
|> Kino.Image.new("image/png")