├── README.md ├── LICENSE └── mondrianomies.R /README.md: -------------------------------------------------------------------------------- 1 | # The Mondrianomies 2 | 3 | This project creates images like this, inspired in neoplasticism based on L-Systems: 4 | 5 | 6 | 7 | ## Getting Started 8 | 9 | ### Prerequisites 10 | 11 | You will need to install the following packages (if you don't have them already): 12 | 13 | ``` 14 | install.packages("gsubfn") 15 | install.packages("tidyverse") 16 | ``` 17 | 18 | ## More info 19 | 20 | The code is simple and is implemented in R. 21 | 22 | A complete explanation of the experiment can be found [at fronkonstin](https://fronkonstin.com/2022/03/25/the-mondrianomies/) 23 | 24 | ## Authors 25 | 26 | * **Antonio Sánchez Chinchón** - [@aschinchon](https://twitter.com/aschinchon) 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Antonio Sánchez Chinchón 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mondrianomies.R: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | library(gsubfn) 3 | library(tidyverse) 4 | 5 | # Number of symbols in rule 6 | s <- sample(15:26, 1) 7 | # Extract s symbols from c("F", "+", "-") randomly 8 | v1 <- sample(c("F", "+", "-"), size = s, replace = TRUE, prob = c(10,12,12)) 9 | # Add 3 pairs of brackets 10 | v2 <- sample("[]", 3, replace = TRUE) %>% str_extract_all("\\d*\\+|\\d*\\-|F|L|R|\\[|\\]|\\|") %>% unlist 11 | # Where to insert brackets 12 | v3 <- sample(1:(s+1), size = length(v2)) %>% sort 13 | # Insert them correctly 14 | for(i in 1:length(v3)){ 15 | c(v1[1:(v3[i] + i - 1)], v2[i], v1[(v3[i] + i - 1):length(v1)]) -> v1 16 | } 17 | 18 | # All ictures start with the same axiom 19 | axiom <- "F-F-F-F" 20 | # Rule to substitute F, as generated previously 21 | rules <- list("F"=paste(v1, collapse="")) 22 | # Turning angle 23 | angle <- 90 24 | # Haw many times to apply the rule 25 | depth <- sample(3:4,1) 26 | # Longitude (factor) of the segments 27 | ds <- jitter(1) 28 | # Substitute axiom depth times 29 | for (i in 1:depth) axiom <- gsubfn(".", rules, axiom) 30 | # Actions that will gneerate the drawing 31 | actions <- str_extract_all(axiom, "\\d*\\+|\\d*\\-|F|L|G|R|\\[|\\]|\\|") %>% unlist 32 | 33 | # These vars store the current position, angle and longitude factor of the point 34 | x_current <- 0 35 | y_current <- 0 36 | a_current <- 0 37 | d_current <- 0 38 | 39 | # To store point position, angle and longitude 40 | status <- tibble(x = x_current, 41 | y = y_current, 42 | alfa = a_current, 43 | depth = d_current) 44 | # To store segments 45 | lines <- data.frame(x = numeric(), 46 | y = numeric(), 47 | xend = numeric(), 48 | yend = numeric()) 49 | 50 | # This loop reads actions and generates the drawing depending on the concrete action 51 | # F -> draw forward 52 | # + -> turn right 53 | # - -> turn left 54 | # [ -> save the current status of point 55 | # ] -> restore the last current status of point and remove from stack 56 | for (action in actions) 57 | { 58 | if (action=="F") { 59 | lines <- lines %>% add_row(x = x_current, 60 | y = y_current, 61 | xend = x_current + (ds^d_current) * cos(a_current * pi / 180), 62 | yend = y_current + (ds^d_current) * sin(a_current * pi / 180)) 63 | x_current <- x_current + (ds^d_current) * cos(a_current * pi / 180) 64 | y_current <- y_current + (ds^d_current) * sin(a_current * pi / 180) 65 | d_current <- d_current + 1 66 | } 67 | if (action=="+") { 68 | a_current <- a_current - angle 69 | } 70 | if (action=="-") { 71 | a_current <- a_current + angle 72 | } 73 | if (action=="[") { 74 | status <- status %>% add_row(x = x_current, 75 | y = y_current, 76 | alfa = a_current, 77 | depth = d_current) 78 | } 79 | if (action=="]") { 80 | x_current <- tail(status, 1) %>% pull(x) 81 | y_current <- tail(status, 1) %>% pull(y) 82 | a_current <- tail(status, 1) %>% pull(alfa) 83 | d_current <- tail(status, 1) %>% pull(depth) 84 | status <- head(status, -1) 85 | } 86 | } 87 | 88 | lines %>% 89 | mutate(x = round(x, 1), 90 | y = round(y, 1), 91 | xend = round(xend, 1), 92 | yend = round(yend, 1)) %>% 93 | distinct(x, y, xend, yend) -> lines 94 | 95 | select(lines, x3 = x, y3 =y) %>% 96 | bind_rows(select(lines, x3 = xend, y3 =yend)) %>% 97 | distinct(x3, y3) -> points 98 | 99 | # Let's find squares to fill inside the drawing 100 | # Since this operation maybe hard to compute, I divide points into 101 | # 10 pieces to process them separately 102 | n <- 10 103 | 104 | split(points, rep(1:ceiling(nrow(points)/n), 105 | each = n, 106 | length.out = nrow(points))) -> points_divided 107 | 108 | # Squares1: add X3, y3 to current segments and filter to find 109 | # right angles 110 | lapply(points_divided, function(sub) { 111 | sub %>% 112 | crossing(lines) %>% 113 | filter(x == x3 | y == y3 | xend == x3 | yend == y3) %>% 114 | filter(x != x3 | y != y3 , xend != x3 | yend != y3) %>% 115 | mutate(id = row_number()) 116 | }) %>% bind_rows() -> squares1 117 | 118 | # Squares2: keep those squares where some of new sides exist in lines 119 | bind_rows( 120 | squares1 %>% 121 | inner_join(lines, c("x" = "x", 122 | "y" = "y", 123 | "x3" = "xend", 124 | "y3" = "yend")), 125 | squares1 %>% 126 | inner_join(lines, c("xend" = "x", 127 | "yend" = "y", 128 | "x3" = "xend", 129 | "y3" = "yend")), 130 | squares1 %>% 131 | inner_join(lines, c("x3" = "x", 132 | "y3" = "y", 133 | "x" = "xend", 134 | "y" = "yend")), 135 | squares1 %>% 136 | inner_join(lines, c("x3" = "x", 137 | "y3" = "y", 138 | "xend" = "xend", 139 | "yend" = "yend"))) %>% 140 | distinct(x, y, xend, yend, x3, y3, id) -> squares2 141 | 142 | # Remove those whose sides form a straight line 143 | squares2 %>% 144 | anti_join(squares2 %>% filter(x == xend, xend == x3), 145 | by = c("x", "y", "xend", "yend", "x3", "y3", "id")) -> squares2 146 | 147 | squares2 %>% 148 | anti_join(squares2 %>% filter(y == yend, yend == y3), 149 | by = c("x", "y", "xend", "yend", "x3", "y3", "id")) -> squares2 150 | 151 | # We leave squares2 prepared for geom_rect 152 | squares2 %>% 153 | mutate(xmax = pmax(x, xend, x3), 154 | xmin = pmin(x, xend, x3), 155 | ymax = pmax(y, yend, y3), 156 | ymin = pmin(y, yend, y3)) %>% 157 | mutate(A = (xmax - xmin) * (ymax - ymin) / 2) -> squares 158 | 159 | # Piet mondrian's palette 160 | colors <- c("#FEFFFA","#000002","#F60201","#FDED01", "#1F7FC9") 161 | 162 | # To remove very small squares I calculate quantiles form its area 163 | qnts <- quantile(squares$A, 164 | probs = seq(0, 1, 0.05), 165 | na.rm = FALSE, 166 | names = TRUE, 167 | type = 7) 168 | 169 | # Here comes the magic of ggplot 170 | ggplot() + 171 | geom_rect(aes(xmax = xmax, 172 | xmin = xmin, 173 | ymax = ymax, 174 | ymin = ymin, 175 | fill = id %% length(colors) %>% jitter(amount=.025)), 176 | data = squares %>% filter(A >= qnts[1]), # remove small squares 177 | lwd = 2, 178 | color = "white") + 179 | geom_segment(aes(x = x, y = y, xend = xend, yend = yend), 180 | data = lines, 181 | lwd = .65, 182 | lineend = "square", 183 | color = "#000002") + 184 | scale_fill_gradientn(colors = colors) + 185 | theme_void() + 186 | theme(legend.position = "none") + 187 | coord_equal() -> plot 188 | 189 | # Calculate dimensions of the picture for ggsave 190 | width <- max(points$x3) - min(points$x3) 191 | height <- max(points$y3) - min(points$y3) 192 | 193 | whmax <- 8 194 | if (width >= height) { 195 | w <- whmax 196 | h <- whmax * height / width 197 | } else { 198 | h <- whmax 199 | w <- whmax * width / height 200 | } 201 | 202 | # Save the drawing with a random name 203 | name <- paste(sample(letters,6), collapse = "") 204 | ggsave(paste0("new/",name,".png"), plot, width = w, height = h) 205 | 206 | 207 | 208 | 209 | 210 | --------------------------------------------------------------------------------