diff --git a/README.md b/README.md index 10feb2f..d6fa1b2 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,41 @@ You should see something like this ## How does it work A vector connects the origin to a specific point `(x, y)`. You can create one -like this: `Vector.build(0, 1)`. +like this: -A box is defined by three vectors, `a`, `b` and `c`. In the picture below, `a` -is red, `b` is orange and `c` is purple. +```elixir +iex(1)> Vector.build(1, 0) +%Vector{x: 1, y: 0} +``` + +Once we have vectors, we can describe geometrical operations on vectors such as +adding, subtracting, negating and scaling them: + +```elixir +iex(1)> Vector.add(Vector.build(1, 0), Vector.build(1, 1)) +%Vector{x: 2, y: 1} + +iex(2)> Vector.sub(Vector.build(1, 0), Vector.build(1, 1)) +%Vector{x: 0, y: -1} + +iex(3)> Vector.neg(Vector.build(1, 2)) +%Vector{x: -1, y: -2} + +iex(4)> Vector.scale(4, Vector.build(1, 0)) +%Vector{x: 4, y: 0} +``` + +A box is defined by three vectors, `a`, `b` and `c`. + +```elixir +box = %Box{ + a: Vector.build(75.0, 75.0), + b: Vector.build(500.0, 0.0), + c: Vector.build(0.0, 500.0) +} +``` + +In the picture below, `a` is red, `b` is orange and `c` is purple. @@ -132,7 +163,7 @@ end This is exactly what the `Fitting.create_picture` function does! -Now let's say we wanted to draw the letter `F`. We could write a function like this: +Now let's say we wanted to draw the letter `f`. We could write a function like this: ```elixir def f do @@ -160,34 +191,416 @@ This would look like this! The amazing thing about defining a picture as an anonymous function is that we can transform the picture by calling functions on the box. For example, if we -had a `turn_box` function that rotated the box left, we could then rotate the +had a `turn` function that rotated the box left, we could then rotate the picture by just calling that. ```elixir -def turn_box(%Box{a: a, b: b, c: c}) do - %Box{ - a: Vector.add(a, b), - b: c, - c: Vector.neg(b) - } +defmodule Box do + def turn(%Box{a: a, b: b, c: c}) do + %Box{ + a: Vector.add(a, b), + b: c, + c: Vector.neg(b) + } + end end -def turn_picture(picture) do - fn box -> - box - |> turn_box() - |> picture.() +defmodule Picture do + def turn(picture) do + fn box -> + box + |> Box.turn() + |> picture.() + end end end ``` -If we applied this code to our `F` we would see something like this: +If we applied this code to our `f` we would see something like this: +In a similar way, we could describe the horizontal flipping of a picture as +an horizontal flipping of the box that contains such picture. -So this is how you would hook the whole thing together: +```elixir +defmodule Box do + def flip(%Box{a: a, b: b, c: c}) do + %Box{ + a: Vector.add(a, b), + b: Vector.neg(b), + c: c + } + end +end + +defmodule Picture do + def flip(picture) do + fn box -> + box + |> Box.flip() + |> picture.() + end + end +end +``` + +And if we applied this to our `f` letter we would obtain this: + + + +What if we wanted to display two pictures side by side? We can think of a +function that takes a box and returns a tuple `{left_box, right_box}`: + +```elixir +defmodule Box do + def split_vertically(%Box{a: a, b: b, c: c}) do + left_box = %Box{ + a: a, + b: Vector.scale(0.5, b), + c: c + } + + right_box = %Box{ + a: Vector.add(a, Vector.scale(0.5, b)), + b: Vector.scale(0.5, b), + c: c + } + + {left_box, right_box} + end +end +``` + +Then we can build a function that takes two pictures and applies the first one +to the left box and the second one to the right box: + +```elixir +defmodule Picture do + def beside(p1, p2) do + fn box -> + {left_box, right_box} = Box.split_vertically(box) + + p1.(left_box) ++ p2.(right_box) + end + end +end +``` + +Applying this function to two `f` letters would look like this: + + + +But what if we applied this function to `f` and `Picture.flip(f)`? Then this +would happen! + + + +Pretty neat, no? + +It turns out we can generalize the above functions to specify a ratio, so we can +control how much space we want to allocate to the left and right. + +```elixir +defmodule Box do + def split_horizontally(factor, %Box{a: a, b: b, c: c}) do + above_ratio = factor + + below_ratio = 1 - above_ratio + + above = %Box{ + a: Vector.add(a, Vector.scale(below_ratio, c)), + b: b, + c: Vector.scale(above_ratio, c) + } + + below = %Box{ + a: a, + b: b, + c: Vector.scale(below_ratio, c) + } + + {above, below} + end +end + +defmodule Picture do + def beside_ratio(m, n, p1, p2) do + fn box -> + factor = m / (m + n) + + {box_left, box_right} = Box.split_vertically(factor, box) + + p1.(box_left) ++ p2.(box_right) + end + end + + def beside(p1, p2) do + beside_ratio(1, 1, p1, p2) + end +end +``` + +With these functions we could write something like `Picture.beside_ratio(1, 2, f, f)` and obtain something like this: + + + +Now imagine that just like our `beside` and `beside_ratio` functions, we would +have another couple of functions that are called `above` and `above_ratio`, +which would position two pictures above one another. You can check out their +implementation in `lib/picture.ex`. + +With those functions in place we can implement a function that takes four +pictures and creates a quartet: + +```elixir +def quartet(p1, p2, p3, p4) do + above( + beside(p1, p2), + beside(p3, p4) + ) +end +``` + +Applying this to four `f` letters would look like this: + + + +Similarly, we could write a function that puts nine pictures together in a +three-by-three grid: + +```elixir +def nonet(p1, p2, p3, p4, p5, p6, p7, p8, p9) do + above_ratio( + 1, + 2, + beside_ratio(1, 2, p1, beside(p2, p3)), + above( + beside_ratio(1, 2, p4, beside(p5, p6)), + beside_ratio(1, 2, p7, beside(p8, p9)) + ) + ) +end +``` + +And if we apply nine `f` letters to this function we would see this: + + +As a fun intermezzo, let's implement a function that takes a picture and throws +it into the air. We will rotate the image by 45 degrees and shrink its area by +half: + +```elixir +defmodule Box do + def toss(%Box{a: a, b: b, c: c}) do + %Box{ + a: Vector.add(a, Vector.scale(0.5, Vector.add(b, c))), + b: Vector.scale(0.5, Vector.add(b, c)), + c: Vector.scale(0.5, Vector.add(c, Vector.neg(b))) + } + end +end + +defmodule Picture do + def toss(picture) do + fn box -> + box + |> Box.toss() + |> picture.() + end + end +end +``` + +You don't need to worry too much about the vector arithmetics, here's what it +would look like: + + + +Cool! + +Now let's take a look at a fish. + + + +This image has some really interesting properties. Let's define a `over` +function that stacks a bunch of pictures on top of one another: + +```elixir +defmodule Picture do + def over(list) when is_list(list) do + fn box -> + Enum.flat_map(list, fn elem -> + elem.(box) + end) + end + end +end +``` + +Applying this to `fish` and `fish |> turn |> turn` yields this + + + +Now we will define a function called `ttile` which is fundamental in building +the fractal structure of the painting. We will see that the fish pattern is +indeed amazing: + +```elixir +def ttile(fish) do + fn box -> + side = fish |> toss |> flip + + over([ + fish, + side |> turn, + side |> turn |> turn + ]).(box) + end +end +``` + + + +And our final basic block is the tile which will build the diagonals of our +square, the `utile` function: + +```elixir +def utile(fish) do + fn box -> + side = fish |> toss |> flip + + over([ + side, + side |> turn, + side |> turn |> turn, + side |> turn |> turn |> turn + ]).(box) + end +end +``` + + + +With these tiles we can now create a recursive function called `side` which +creates the side of our square. This function will take a parameter which +specifies the depth of the recursion. + +``` +def side(0, _fish), do: fn _ -> [] end + +def side(n, fish) when n > 0 do + fn box -> + quartet( + side(n - 1, fish), + side(n - 1, fish), + turn(ttile(fish)), + ttile(fish) + ).(box) + end +end +``` + +- If we pass `0`, we will just have an empty picture. +- If we pass `1`, we wil have something that looks like this + + + +- If we pass `2`, we wil have something that looks like this + + + +This is starting to look great! + +Now that we have our sides, we can build corners! `corner` is another recursive +function that will take a parameter which denotes the depth of the recursion: + +```elixir +def corner(0, _fish), do: fn _ -> [] end + +def corner(n, fish) when n > 0 do + fn box -> + quartet( + corner(n - 1, fish), + side(n - 1, fish), + side(n - 1, fish) |> turn, + utile(fish) + ).(box) + end +end +``` + +- If we pass `0`, we will just have an empty picture. +- If we pass `1`, we wil have something that looks like this + + + +- If we pass `2`, we wil have something that looks like this + + + +Finally, the last step! + +By combining a central `utile` and a sequence of `side` and `corner` we can +implement the Square Limit. + +```elixir +def square_limit(0, _fish), do: fn _ -> [] end + +def square_limit(n, fish) when n > 0 do + fn box -> + corner = corner(n - 1, fish) + side = side(n - 1, fish) + + nw = corner + nc = side + ne = corner |> turn |> turn |> turn + mw = side |> turn + mc = utile(fish) + me = side |> turn |> turn |> turn + sw = corner |> turn + sc = side |> turn |> turn + se = corner |> turn |> turn + + nonet( + nw, + nc, + ne, + mw, + mc, + me, + sw, + sc, + se + ).(box) + end +end +``` + +Let's see how it looks with different depths: + +- With depth `1`, it's just a `utile` + + + +- With depth `2`, it's a `utile` surrounded by a line of sides and corners + + + +- With depth `3`, we've added another layer around the previous one + + + +- With depth `5`, it's looking really great + + + +Mission complete! + +## Scenic integration + +So this is how you would hook the whole thing together: ```elixir Graph.build(font: :roboto, font_size: 24, theme: :light) diff --git a/images/fish.png b/images/fish.png new file mode 100644 index 0000000..934cf41 Binary files /dev/null and b/images/fish.png differ diff --git a/images/fish_corner_1.png b/images/fish_corner_1.png new file mode 100644 index 0000000..cec967d Binary files /dev/null and b/images/fish_corner_1.png differ diff --git a/images/fish_corner_2.png b/images/fish_corner_2.png new file mode 100644 index 0000000..f16f33f Binary files /dev/null and b/images/fish_corner_2.png differ diff --git a/images/fish_over.png b/images/fish_over.png new file mode 100644 index 0000000..d69d85c Binary files /dev/null and b/images/fish_over.png differ diff --git a/images/fish_side_1.png b/images/fish_side_1.png new file mode 100644 index 0000000..c7554ba Binary files /dev/null and b/images/fish_side_1.png differ diff --git a/images/fish_side_2.png b/images/fish_side_2.png new file mode 100644 index 0000000..591b59b Binary files /dev/null and b/images/fish_side_2.png differ diff --git a/images/fish_ttile.png b/images/fish_ttile.png new file mode 100644 index 0000000..4365aa8 Binary files /dev/null and b/images/fish_ttile.png differ diff --git a/images/fish_utile.png b/images/fish_utile.png new file mode 100644 index 0000000..72d8626 Binary files /dev/null and b/images/fish_utile.png differ diff --git a/images/letter_f_beside.png b/images/letter_f_beside.png new file mode 100644 index 0000000..8599353 Binary files /dev/null and b/images/letter_f_beside.png differ diff --git a/images/letter_f_beside_flipped.png b/images/letter_f_beside_flipped.png new file mode 100644 index 0000000..868050e Binary files /dev/null and b/images/letter_f_beside_flipped.png differ diff --git a/images/letter_f_beside_ratio.png b/images/letter_f_beside_ratio.png new file mode 100644 index 0000000..d079378 Binary files /dev/null and b/images/letter_f_beside_ratio.png differ diff --git a/images/letter_f_flipped.png b/images/letter_f_flipped.png new file mode 100644 index 0000000..b6c5626 Binary files /dev/null and b/images/letter_f_flipped.png differ diff --git a/images/letter_f_nonet.png b/images/letter_f_nonet.png new file mode 100644 index 0000000..9bbc6d2 Binary files /dev/null and b/images/letter_f_nonet.png differ diff --git a/images/letter_f_quartet.png b/images/letter_f_quartet.png new file mode 100644 index 0000000..762b89b Binary files /dev/null and b/images/letter_f_quartet.png differ diff --git a/images/letter_f_tossed.png b/images/letter_f_tossed.png new file mode 100644 index 0000000..fc60230 Binary files /dev/null and b/images/letter_f_tossed.png differ diff --git a/images/square_limit_1.png b/images/square_limit_1.png new file mode 100644 index 0000000..890fda7 Binary files /dev/null and b/images/square_limit_1.png differ diff --git a/images/square_limit_2.png b/images/square_limit_2.png new file mode 100644 index 0000000..0ce1b5e Binary files /dev/null and b/images/square_limit_2.png differ diff --git a/images/square_limit_3.png b/images/square_limit_3.png new file mode 100644 index 0000000..7f49e5b Binary files /dev/null and b/images/square_limit_3.png differ diff --git a/lib/box.ex b/lib/box.ex index 6595f88..6d6f379 100644 --- a/lib/box.ex +++ b/lib/box.ex @@ -18,7 +18,7 @@ defmodule Box do end def flip(%Box{a: a, b: b, c: c}) do - %Box{a: Vector.add(a, c), b: b, c: Vector.neg(c)} + %Box{a: Vector.add(a, b), b: Vector.neg(b), c: c} end def toss(%Box{a: a, b: b, c: c}) do diff --git a/lib/picture.ex b/lib/picture.ex index 88fab72..a187e6e 100644 --- a/lib/picture.ex +++ b/lib/picture.ex @@ -93,12 +93,12 @@ defmodule Picture do def ttile(fish) do fn box -> - side = fish |> toss |> flip + side = fish |> toss over([ fish, - side |> turn, - side |> turn |> turn + side |> flip, + side |> turn |> flip ]).(box) end end diff --git a/lib/scenes/home.ex b/lib/scenes/home.ex index 9bb6019..f1ba53f 100644 --- a/lib/scenes/home.ex +++ b/lib/scenes/home.ex @@ -4,6 +4,7 @@ defmodule ScenicEscher.Scene.Home do alias Scenic.Graph import Scenic.Primitives + import Picture @graph Graph.build(font: :roboto, font_size: 24, theme: :light) |> group( @@ -11,8 +12,8 @@ defmodule ScenicEscher.Scene.Home do # We create a box box = %Box{ a: Vector.build(75.0, 75.0), - b: Vector.build(500.0, 0.0), - c: Vector.build(0.0, 500.0) + b: Vector.build(400.0, 0.0), + c: Vector.build(0.0, 400.0) } # We create a fish @@ -34,7 +35,7 @@ defmodule ScenicEscher.Scene.Home do path(acc, elem, options) end) end, - translate: {20, 60} + translate: {75, 75} ) def init(_, _) do