├── .gitignore ├── README_files └── figure-gfm │ ├── unnamed-chunk-3-1.png │ ├── unnamed-chunk-4-1.png │ ├── unnamed-chunk-6-1.png │ ├── unnamed-chunk-9-1.png │ ├── unnamed-chunk-11-1.png │ ├── unnamed-chunk-12-1.png │ ├── unnamed-chunk-12-2.png │ ├── unnamed-chunk-13-1.png │ ├── unnamed-chunk-13-2.png │ ├── unnamed-chunk-15-1.png │ ├── unnamed-chunk-15-2.png │ ├── unnamed-chunk-15-3.png │ ├── unnamed-chunk-15-4.png │ ├── unnamed-chunk-16-1.png │ ├── unnamed-chunk-16-2.png │ ├── unnamed-chunk-16-3.png │ ├── unnamed-chunk-16-4.png │ ├── unnamed-chunk-17-1.png │ ├── unnamed-chunk-18-1.png │ ├── unnamed-chunk-18-2.png │ ├── unnamed-chunk-19-1.png │ ├── unnamed-chunk-20-1.png │ └── unnamed-chunk-21-1.png ├── README.Rmd └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-3-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-4-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-6-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-9-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-9-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-11-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-11-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-12-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-12-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-12-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-12-2.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-13-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-13-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-13-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-13-2.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-15-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-15-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-15-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-15-2.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-15-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-15-3.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-15-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-15-4.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-16-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-16-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-16-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-16-2.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-16-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-16-3.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-16-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-16-4.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-17-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-17-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-18-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-18-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-18-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-18-2.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-19-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-19-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-20-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-20-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-21-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cj-holmes/photos-on-spirals/HEAD/README_files/figure-gfm/unnamed-chunk-21-1.png -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | editor_options: 4 | chunk_output_type: console 5 | --- 6 | 7 | 8 | 9 | ```{r, include = FALSE} 10 | knitr::opts_chunk$set( 11 | collapse = TRUE, 12 | comment = "#>" 13 | ) 14 | ``` 15 | 16 | # Photos on spirals 17 | 18 | ## Introduction 19 | 20 | * A while ago I saw an example of a cool effect where a photo was recreated on a single line spiral. The image was rendered by the spiral line getting thicker (to create darker areas) and thinner (to create lighter areas). 21 | * I can't find the original example I saw, but I have since found this website [https://spiralbetty.com/](https://spiralbetty.com/) - which seems to do the same thing in a really nice way! 22 | * So this is my attempt at a janky implementation of this effect in R using spatial intersections, driven by the `{sf}` package 23 | * This implementation doesn't create a truly continuous single line image, but rather sections that are plotted with different thickness values 24 | * This code is inefficient and could contain errors - its just for fun to try and create these images! 25 | 26 | ```{r warning=FALSE, message=FALSE} 27 | library(magick) 28 | library(sf) 29 | library(tidyverse) 30 | ``` 31 | 32 | ## Read an image 33 | * Read a test image. I choose Audrey! 34 | ```{r fig.width = 1} 35 | i <- 36 | image_read('https://images.photowall.com/products/59143/audrey-hepburn-3.jpg?h=699&q=85') |> 37 | image_resize("300x") 38 | 39 | i 40 | ``` 41 | 42 | ## Process image 43 | * Crop it square 44 | * Convert it to greyscale 45 | * Reduce the number of grey shades 46 | * Flip it to be upside down (more on this later!) 47 | ```{r} 48 | # Define some variables for this step 49 | size <- 300 50 | n_shades <- 16 51 | 52 | i_processed <- 53 | i |> 54 | image_resize(paste0(size,"x",size,"^")) |> 55 | image_crop(geometry = paste0(size,"x",size), gravity = "center") |> 56 | image_convert(type = "grayscale") |> 57 | image_quantize(max = n_shades, dither=FALSE) |> 58 | image_flip() 59 | 60 | i_processed 61 | ``` 62 | 63 | ## Convert image to polygons 64 | * Convert the image to a dataframe 65 | * Extract the red green and blue values for each pixel (they're the same as I previously converted the image to greyscale) 66 | * Rescale the red channel (could be done with green or blue) to be between 1 and 0 67 | * Convert the dataframe to a `{stars}` raster object 68 | * Convert the `{stars}` raster to an `{sf}` polygon set merging cells with identical values 69 | * Make the polygons valid and normalise to be between 0 and 1 (this works because I cropped the image to be square originally) 70 | * This whole step feels clunky. I'd love to know if there is a more efficient/elegant way of converting an image raster to a set of polygons 71 | ```{r} 72 | i_sf <- 73 | i_processed |> 74 | image_raster() |> 75 | mutate( 76 | col2rgb(col) |> t() |> as_tibble(), 77 | col = scales::rescale(red, to = c(1,0))) |> 78 | select(-green, -blue, -red) |> 79 | stars::st_as_stars() |> 80 | st_as_sf(as_points = FALSE, merge = TRUE) |> 81 | st_make_valid() |> 82 | st_set_agr("constant") |> 83 | st_normalize() 84 | ``` 85 | 86 | * Visualise the `{sf}` polygons 87 | * Notice that the image is not plotting upside down (this is because we flipped it earlier) 88 | * It would of course be possible to reverse the y axis instead - but then this will cause problems when adding other (non inverted) layers to the plot 89 | ```{r} 90 | ggplot() + 91 | geom_sf(data = i_sf, col = NA, aes(fill = col)) + 92 | scale_fill_viridis_c(direction = -1) 93 | ``` 94 | 95 | ## Generate spiral 96 | * A function for a parameterised spiral that I cobbled together from various online sources and some of my own ropey maths 97 | * This is probably not great but works well enough for this project 98 | ```{r} 99 | #' Create spiral coordinates 100 | #' 101 | #' @param xo Spiral origin x coordinate 102 | #' @param yo Spiral origin y coordinate 103 | #' @param n_points Number of points on whole spiral (equally spaced in angle) 104 | #' @param n_turns Number of turns in spiral 105 | #' @param r0 Spiral inner radius 106 | #' @param r1 Spiral outer radius 107 | #' @param offset_angle Offset angle for start of spiral (in degrees) 108 | spiral_coords <- function(xo, yo, n_points, n_turns, r0, r1, offset_angle){ 109 | 110 | b <- (r1 - r0)/(2*pi*n_turns) 111 | l <- seq(0, 2*pi*n_turns, l=n_points) 112 | 113 | tibble( 114 | x = (r0 + (b*l))*cos(l + offset_angle*(pi/180)) + xo, 115 | y = (r0 + (b*l))*sin(l + offset_angle*(pi/180)) + yo) 116 | } 117 | ``` 118 | 119 | * Define some parameters for the spiral 120 | * Create the spiral coordinates and convert it to an `{sf}` linestring 121 | ```{r} 122 | n_turns <- 50 123 | spiral_r1 <- 0.5 124 | 125 | spiral <- 126 | spiral_coords( 127 | xo = 0.5, 128 | yo = 0.5, 129 | n_points = 5000, 130 | n_turns = n_turns, 131 | r0 = 0, 132 | r1 = spiral_r1, 133 | offset_angle = 0) |> 134 | as.matrix() |> 135 | sf::st_linestring() 136 | ``` 137 | 138 | * Visualise the `{sf}` spiral and image polygons as a sense check that they align 139 | ```{r} 140 | ggplot()+ 141 | geom_sf(data = i_sf, aes(fill = col), col = NA) + 142 | geom_sf(data = spiral, col = "black") + 143 | scale_fill_viridis_c("", alpha = 0.75, direction = -1)+ 144 | theme(legend.position = "") 145 | ``` 146 | 147 | ## Compute spiral - image interscetions 148 | * Compute the minimum `thin` and maximum `thick` amount to buffer the intersections by 149 | * The `thick` value is set to be 95% of the gap between the spiral lines (so it will always scale appropriately) 150 | * Compute image spiral intersections 151 | * Rescale the greyscale value to be between `thick` and `thin` so that darker shades are closer to `thick` and lighter shades are closer to `thin` 152 | * Buffer the intersections by their scaled greyscale values 153 | ```{r} 154 | thin <- 0.00025 155 | thick <- ((spiral_r1/n_turns)/2)*0.95 156 | 157 | intersections <- 158 | st_intersection(i_sf, spiral) |> 159 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 160 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 161 | ``` 162 | 163 | * Visualise the spiral image 164 | ```{r fig.width=8, fig.height=8, dpi=300} 165 | ggplot() + geom_sf(data = intersections, fill = "black", col = NA) 166 | ``` 167 | 168 | * The colour can also be mapped to the colour shade - but this will ruin the illusion of a continuous line 169 | * I arrange the data by the thickness value descending prior to plotting so that the thinner lines are plotted on top of the thicker ones and don't get lost 170 | ```{r fig.width=8, fig.height=8, dpi=300, out.width="50%", fig.show='hold'} 171 | ggplot() + 172 | geom_sf( 173 | data = intersections |> arrange(desc(n)), 174 | aes (fill = n), 175 | col = NA) + 176 | scale_fill_gradient(low = "white", high = "black")+ 177 | theme_void()+ 178 | theme(legend.position = "") 179 | 180 | ggplot() + 181 | geom_sf( 182 | data = intersections |> arrange(desc(n)), 183 | aes (fill = n), 184 | col = NA) + 185 | scale_fill_viridis_c(direction = -1, option = "plasma")+ 186 | theme_void()+ 187 | theme(legend.position = "") 188 | ``` 189 | 190 | * A single polygon of the spiral can be made with `st_union()`. The boundary linestring could also be returned afterwards by using `st_boundary()` 191 | ```{r fig.width=8, fig.height=8, dpi=300, out.width="50%", fig.show='hold'} 192 | ggplot() + geom_sf(data = intersections |> st_union(), col = 1, fill = "red") 193 | last_plot() + coord_sf(xlim = c(0.4, 0.6), ylim = c(0.4, 0.6)) 194 | ``` 195 | 196 | ## Create a function 197 | * Put together a function that runs the code above, with some small additions 198 | * The ability to invert the image 199 | * Control the outer spiral radius by a factor `spiral_r1_f` where a value of 1 takes it to the edge of the image 200 | * Parameterise the spiral max thickness with a factor `thick_f` where a value of 1 should make the spiral touch in the thickest areas 201 | * Note that this function also calls the `spiral_cords()` function defined above 202 | 203 | ```{r} 204 | spiral_image <- 205 | function( 206 | img, 207 | invert = FALSE, 208 | size = 300, 209 | n_shades = 16, 210 | spiral_points = 5000, 211 | spiral_turns = 50, 212 | spiral_r0 = 0, 213 | spiral_r1_f = 1, 214 | thin = 0.00025, 215 | thick_f = 0.95, 216 | spiral_offset_angle = 0, 217 | col_line = "black", 218 | col_bg = "white"){ 219 | 220 | # Read image -------------------------------------------------------------- 221 | if(class(img) == "magick-image"){i <- img} else {i <- magick::image_read(img)} 222 | 223 | # Process image to sf polygon 224 | i_sf <- 225 | i |> 226 | magick::image_resize(paste0(size,"x",size,"^")) |> 227 | magick::image_crop(geometry = paste0(size,"x",size), gravity = "center") |> 228 | magick::image_convert(type = "grayscale") |> 229 | magick::image_quantize(max = n_shades, dither=FALSE) |> 230 | magick::image_flip() |> 231 | magick::image_raster() |> 232 | dplyr::mutate( 233 | col2rgb(col) |> t() |> as_tibble(), 234 | col = scales::rescale(red, to = if(invert){c(0,1)}else{c(1, 0)})) |> 235 | dplyr::select(-green, -blue, -red) |> 236 | stars::st_as_stars() |> 237 | sf::st_as_sf(as_points = FALSE, merge = TRUE) |> 238 | sf::st_make_valid() |> 239 | sf::st_set_agr("constant") |> 240 | sf::st_normalize() 241 | 242 | # Generate spiral ---------------------------------------------------------- 243 | spiral <- 244 | spiral_coords( 245 | xo = 0.5, 246 | yo = 0.5, 247 | n_points = spiral_points, 248 | n_turns = spiral_turns, 249 | r0 = spiral_r0, 250 | r1 = 0.5 * spiral_r1_f, 251 | offset_angle = spiral_offset_angle) |> 252 | as.matrix() |> 253 | sf::st_linestring() 254 | 255 | # Compute the thick value 256 | thick <- ((((0.5*spiral_r1_f) - spiral_r0)/spiral_turns)/2)*thick_f 257 | 258 | intersections <- 259 | sf::st_intersection(i_sf, spiral) |> 260 | dplyr::mutate(n = scales::rescale(col, to=c(thin, thick))) |> 261 | dplyr::mutate(geometry = sf::st_buffer(geometry, n, endCapStyle = "ROUND")) |> 262 | sf::st_union() 263 | 264 | ggplot2::ggplot() + 265 | ggplot2::geom_sf(data = intersections, fill = col_line, col = NA)+ 266 | ggplot2::theme_void()+ 267 | ggplot2::theme(panel.background = ggplot2::element_rect(fill = col_bg, colour = NA))+ 268 | ggplot2::scale_x_continuous(limits = c(0,1))+ 269 | ggplot2::scale_y_continuous(limits = c(0,1)) 270 | } 271 | ``` 272 | 273 | * Some examples 274 | ```{r fig.width=8, fig.height=8, dpi=300, fig.show='hold', out.width="50%"} 275 | spiral_image(i) 276 | spiral_image(i, invert = TRUE) 277 | spiral_image(i, col_bg = "grey20", col_line = "hotpink", invert = TRUE) 278 | spiral_image(i, spiral_r0 = 0.1) 279 | ``` 280 | 281 | * Some interesting effects can be achieved by setting the `spiral_turns` to be specific fractions of `spiral_points` 282 | ```{r fig.width=8, fig.height=8, dpi=300, fig.show='hold', out.width="50%"} 283 | spiral_image(i, spiral_points = 150, spiral_turns = 50, thick_f = 0.5) 284 | 285 | spiral_image(i, spiral_points = 200, spiral_turns = 50, thick_f = 0.5) 286 | spiral_image(i, spiral_points = 200, spiral_turns = 50, thick_f = 0.5, spiral_offset_angle = 45) 287 | 288 | spiral_image( 289 | i, 290 | spiral_points = 250, 291 | spiral_turns = 50, 292 | invert = TRUE, 293 | col_bg = "grey10", 294 | col_line = "cyan", 295 | thick_f = 0.5) 296 | ``` 297 | 298 | 299 | ## Double image on spiral 300 | * One last experiment 301 | * Using the ability in `st_buffer()` to buffer only one side of the spiral line, it's possible to render two images on the same spiral 302 | * Here I read a second image of audrey 303 | 304 | ```{r fig.width = 1} 305 | i2 <- 306 | magick::image_read('https://images.photowall.com/products/59144/audrey-hepburn-in-breakfast-at-tiffanys-1.jpg?h=699&q=85') |> 307 | magick::image_resize("300x") 308 | 309 | i2 310 | ``` 311 | 312 | * Process the image as before 313 | * When it comes to the intersections, I buffer one with positive values and one with negative values and set `singleSide = TRUE` 314 | * It doesn't look like the `endCapStyle = "ROUND"` works for me when buffering on a single side... 315 | * Then visualise both sets of intersections with different colours 316 | * The fact that we normalised the `{sf}` polygon images to be between 0 and 1 makes this fairly trivial 317 | ```{r fig.width=8, fig.height=8, dpi=300} 318 | i2_sf <- 319 | i2 |> 320 | image_resize(paste0("300x300^")) |> 321 | image_crop(geometry = paste0("300x300"), gravity = "center") |> 322 | image_convert(type = "grayscale") |> 323 | image_quantize(max = 16, dither=FALSE) |> 324 | image_flip() |> 325 | image_raster() |> 326 | mutate( 327 | col2rgb(col) |> t() |> as_tibble(), 328 | col = scales::rescale(red, to = c(1, 0))) |> 329 | select(-green, -blue, -red) |> 330 | stars::st_as_stars() |> 331 | st_as_sf(as_points = FALSE, merge = TRUE) |> 332 | st_set_agr("constant") |> 333 | st_normalize() |> 334 | st_make_valid() 335 | 336 | intersections_1 <- 337 | st_intersection(i_sf, spiral) |> 338 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 339 | mutate(geometry = st_buffer(geometry, n, singleSide = TRUE)) 340 | 341 | intersections_2 <- 342 | st_intersection(i2_sf, spiral) |> 343 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 344 | filter(st_geometry_type(geometry) != "POINT") |> # need to remove these for a negative buffer(?) 345 | mutate(geometry = st_buffer(geometry, -n, singleSide = TRUE)) 346 | 347 | ggplot() + 348 | geom_sf(data = intersections_1, fill = 2, col = NA)+ 349 | geom_sf(data = intersections_2, fill = 4, col = NA) 350 | last_plot() + coord_sf(xlim = c(0.3, 0.7), ylim = c(0.3, 0.7)) 351 | ``` 352 | 353 | ## Beyond spirals 354 | * Of course this method lends itself to any shapes being used (not just spirals) 355 | 356 | ### Grids 357 | * Here's a very quick look at using a grid of squares or hexagons instead using `st_make_grid()` 358 | * Just using the same values of `thin` and `thick` as before. This should be changed 359 | 360 | ```{r fig.width=8, fig.height=8, dpi=300} 361 | square_grid <- st_make_grid(i_sf, n = 50, square = TRUE) |> st_boundary() |> st_union() 362 | 363 | intersections <- 364 | st_intersection(i_sf, square_grid) |> 365 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 366 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 367 | 368 | ggplot() + geom_sf(data = intersections, fill = 1, col = NA) 369 | ``` 370 | 371 | 372 | ```{r fig.width=8, fig.height=8, dpi=300} 373 | hex_grid <- st_make_grid(i_sf, n = 50, square = FALSE) |> st_boundary() |> st_union() 374 | 375 | intersections <- 376 | st_intersection(i_sf, hex_grid) |> 377 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 378 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 379 | 380 | ggplot() + geom_sf(data = intersections, fill = 1, col = NA) 381 | ``` 382 | 383 | ### Flow field 384 | * I've modified some code from my [flow field art(?) repo](https://github.com/cj-holmes/flow-field-art) to create a set of LINESTRINGs across a flow field 385 | * This is just a quick proof of concept 386 | ```{r fig.width=8, fig.height=8, dpi=300} 387 | set.seed(4) 388 | 389 | # Set dimensions for noise generation 390 | x_side <- 400 391 | y_side <- 400 392 | 393 | # Create a noise field matrix with the {ambient} package 394 | m <- 395 | ambient::noise_simplex( 396 | c(y_side, x_side), 397 | frequency = 0.0003, 398 | octaves = 1, 399 | pertubation = "normal", 400 | pertubation_amplitude = 2, 401 | fractal = 'billow') |> 402 | scales::rescale(c(-90, 90)) # scale noise values to angles in degrees (-90 to 90) 403 | 404 | # Get the coords of flow line across the angle matrix 405 | ff_polys <- function( 406 | x_start, 407 | y_start, 408 | step_length, 409 | n_steps, 410 | angle_matrix){ 411 | 412 | # Initialise vectors with the starting x and y values filled with NAs 413 | out_x <- c(x_start, rep(NA, n_steps)) 414 | out_y <- c(y_start, rep(NA, n_steps)) 415 | 416 | # If the starting point is outside the angle_matrix dimensions, return NULL 417 | if(x_start > ncol(angle_matrix) | 418 | x_start < 1 | 419 | y_start > nrow(angle_matrix) | 420 | y_start < 1){ 421 | return(NULL) 422 | } 423 | 424 | # Loop through each step as we travel across the angle matrix 425 | for(i in 1:n_steps){ 426 | 427 | # Get the angle of the nearest flow field point where we are for this iteration 428 | a <- angle_matrix[round(out_y[i]), round(out_x[i])] 429 | 430 | # Compute how far to move in x and y for the given angle and step_length 431 | step_x <- cos(a*(pi/180))*step_length 432 | step_y <- sin(a*(pi/180))*step_length 433 | 434 | # Add the distance in x and y to the current location 435 | next_x <- out_x[i] + step_x 436 | next_y <- out_y[i] + step_y 437 | 438 | # If the next point in the path sits outside the angle matrix, stop iterating along the path 439 | if(next_x > ncol(angle_matrix) | 440 | next_x < 1 | 441 | next_y > nrow(angle_matrix) | 442 | next_y < 1){ 443 | break 444 | } 445 | 446 | # Append the new x and y location to the output 447 | # (ready to be used as the starting point for the next step iteration) 448 | out_x[i+1] <- next_x 449 | out_y[i+1] <- next_y 450 | } 451 | 452 | # Return tibble of the x, y, paths 453 | tibble(x = out_x, y = out_y) |> filter(!is.na(x), !is.na(y)) 454 | } 455 | 456 | # Define number of points for flow lines to start at 457 | n <- 800 458 | 459 | # Re assign thick and thin 460 | thin <- 0.0001 461 | thick <- 0.0025 462 | 463 | # Compute the flow line LINESTRINGs 464 | ff <- 465 | tibble( 466 | x_start = runif(n, 1, ncol(m)), 467 | y_start = runif(n, 1, nrow(m))) |> 468 | mutate( 469 | id = row_number(), 470 | step_length = 1, 471 | n_steps = 400) |> 472 | mutate( 473 | paths = pmap( 474 | .l = list(x_start = x_start, 475 | y_start = y_start, 476 | step_length = step_length, 477 | n_steps = n_steps), 478 | .f = ff_polys, 479 | angle_matrix = m)) |> 480 | unnest(cols=paths) |> 481 | st_as_sf(coords = c("x", "y")) |> 482 | group_by(id) |> 483 | summarise() |> 484 | mutate(type = st_geometry_type(geometry)) |> 485 | filter(type == "MULTIPOINT") |> 486 | st_cast("LINESTRING") |> 487 | st_union() |> 488 | st_normalize() 489 | 490 | # Compute intersections and plot 491 | intersections <- 492 | st_intersection(i_sf, ff) |> 493 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 494 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 495 | 496 | ggplot() + geom_sf(data = intersections, fill = 1, col = NA) 497 | ``` 498 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Photos on spirals 5 | 6 | ## Introduction 7 | 8 | - A while ago I saw an example of a cool effect where a photo was 9 | recreated on a single line spiral. The image was rendered by the 10 | spiral line getting thicker (to create darker areas) and thinner (to 11 | create lighter areas). 12 | - I can’t find the original example I saw, but I have since found this 13 | website - which seems to do the same 14 | thing in a really nice way! 15 | - So this is my attempt at a janky implementation of this effect in R 16 | using spatial intersections, driven by the `{sf}` package 17 | - This implementation doesn’t create a truly continuous single 18 | line image, but rather sections that are plotted with different 19 | thickness values 20 | - This code is inefficient and could contain errors - its just for 21 | fun to try and create these images! 22 | 23 | ``` r 24 | library(magick) 25 | library(sf) 26 | library(tidyverse) 27 | ``` 28 | 29 | ## Read an image 30 | 31 | - Read a test image. I choose Audrey! 32 | 33 | ``` r 34 | i <- 35 | image_read('https://images.photowall.com/products/59143/audrey-hepburn-3.jpg?h=699&q=85') |> 36 | image_resize("300x") 37 | 38 | i 39 | ``` 40 | 41 | 42 | 43 | ## Process image 44 | 45 | - Crop it square 46 | - Convert it to greyscale 47 | - Reduce the number of grey shades 48 | - Flip it to be upside down (more on this later!) 49 | 50 | ``` r 51 | # Define some variables for this step 52 | size <- 300 53 | n_shades <- 16 54 | 55 | i_processed <- 56 | i |> 57 | image_resize(paste0(size,"x",size,"^")) |> 58 | image_crop(geometry = paste0(size,"x",size), gravity = "center") |> 59 | image_convert(type = "grayscale") |> 60 | image_quantize(max = n_shades, dither=FALSE) |> 61 | image_flip() 62 | 63 | i_processed 64 | ``` 65 | 66 | 67 | 68 | ## Convert image to polygons 69 | 70 | - Convert the image to a dataframe 71 | - Extract the red green and blue values for each pixel (they’re the 72 | same as I previously converted the image to greyscale) 73 | - Rescale the red channel (could be done with green or blue) to be 74 | between 1 and 0 75 | - Convert the dataframe to a `{stars}` raster object 76 | - Convert the `{stars}` raster to an `{sf}` polygon set merging cells 77 | with identical values 78 | - Make the polygons valid and normalise to be between 0 and 1 (this 79 | works because I cropped the image to be square originally) 80 | - This whole step feels clunky. I’d love to know if there is a more 81 | efficient/elegant way of converting an image raster to a set of 82 | polygons 83 | 84 | ``` r 85 | i_sf <- 86 | i_processed |> 87 | image_raster() |> 88 | mutate( 89 | col2rgb(col) |> t() |> as_tibble(), 90 | col = scales::rescale(red, to = c(1,0))) |> 91 | select(-green, -blue, -red) |> 92 | stars::st_as_stars() |> 93 | st_as_sf(as_points = FALSE, merge = TRUE) |> 94 | st_make_valid() |> 95 | st_set_agr("constant") |> 96 | st_normalize() 97 | ``` 98 | 99 | - Visualise the `{sf}` polygons 100 | - Notice that the image is not plotting upside down (this is because 101 | we flipped it earlier) 102 | - It would of course be possible to reverse the y axis instead - 103 | but then this will cause problems when adding other (non 104 | inverted) layers to the plot 105 | 106 | ``` r 107 | ggplot() + 108 | geom_sf(data = i_sf, col = NA, aes(fill = col)) + 109 | scale_fill_viridis_c(direction = -1) 110 | ``` 111 | 112 | ![](README_files/figure-gfm/unnamed-chunk-6-1.png) 113 | 114 | ## Generate spiral 115 | 116 | - A function for a parameterised spiral that I cobbled together from 117 | various online sources and some of my own ropey maths 118 | - This is probably not great but works well enough for this project 119 | 120 | ``` r 121 | #' Create spiral coordinates 122 | #' 123 | #' @param xo Spiral origin x coordinate 124 | #' @param yo Spiral origin y coordinate 125 | #' @param n_points Number of points on whole spiral (equally spaced in angle) 126 | #' @param n_turns Number of turns in spiral 127 | #' @param r0 Spiral inner radius 128 | #' @param r1 Spiral outer radius 129 | #' @param offset_angle Offset angle for start of spiral (in degrees) 130 | spiral_coords <- function(xo, yo, n_points, n_turns, r0, r1, offset_angle){ 131 | 132 | b <- (r1 - r0)/(2*pi*n_turns) 133 | l <- seq(0, 2*pi*n_turns, l=n_points) 134 | 135 | tibble( 136 | x = (r0 + (b*l))*cos(l + offset_angle*(pi/180)) + xo, 137 | y = (r0 + (b*l))*sin(l + offset_angle*(pi/180)) + yo) 138 | } 139 | ``` 140 | 141 | - Define some parameters for the spiral 142 | - Create the spiral coordinates and convert it to an `{sf}` linestring 143 | 144 | ``` r 145 | n_turns <- 50 146 | spiral_r1 <- 0.5 147 | 148 | spiral <- 149 | spiral_coords( 150 | xo = 0.5, 151 | yo = 0.5, 152 | n_points = 5000, 153 | n_turns = n_turns, 154 | r0 = 0, 155 | r1 = spiral_r1, 156 | offset_angle = 0) |> 157 | as.matrix() |> 158 | sf::st_linestring() 159 | ``` 160 | 161 | - Visualise the `{sf}` spiral and image polygons as a sense check that 162 | they align 163 | 164 | ``` r 165 | ggplot()+ 166 | geom_sf(data = i_sf, aes(fill = col), col = NA) + 167 | geom_sf(data = spiral, col = "black") + 168 | scale_fill_viridis_c("", alpha = 0.75, direction = -1)+ 169 | theme(legend.position = "") 170 | ``` 171 | 172 | ![](README_files/figure-gfm/unnamed-chunk-9-1.png) 173 | 174 | ## Compute spiral - image interscetions 175 | 176 | - Compute the minimum `thin` and maximum `thick` amount to buffer the 177 | intersections by 178 | - The `thick` value is set to be 95% of the gap between the spiral 179 | lines (so it will always scale appropriately) 180 | - Compute image spiral intersections 181 | - Rescale the greyscale value to be between `thick` and `thin` so that 182 | darker shades are closer to `thick` and lighter shades are closer to 183 | `thin` 184 | - Buffer the intersections by their scaled greyscale values 185 | 186 | ``` r 187 | thin <- 0.00025 188 | thick <- ((spiral_r1/n_turns)/2)*0.95 189 | 190 | intersections <- 191 | st_intersection(i_sf, spiral) |> 192 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 193 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 194 | ``` 195 | 196 | - Visualise the spiral image 197 | 198 | ``` r 199 | ggplot() + geom_sf(data = intersections, fill = "black", col = NA) 200 | ``` 201 | 202 | ![](README_files/figure-gfm/unnamed-chunk-11-1.png) 203 | 204 | - The colour can also be mapped to the colour shade - but this will 205 | ruin the illusion of a continuous line 206 | - I arrange the data by the thickness value descending prior to 207 | plotting so that the thinner lines are plotted on top of the thicker 208 | ones and don’t get lost 209 | 210 | ``` r 211 | ggplot() + 212 | geom_sf( 213 | data = intersections |> arrange(desc(n)), 214 | aes (fill = n), 215 | col = NA) + 216 | scale_fill_gradient(low = "white", high = "black")+ 217 | theme_void()+ 218 | theme(legend.position = "") 219 | 220 | ggplot() + 221 | geom_sf( 222 | data = intersections |> arrange(desc(n)), 223 | aes (fill = n), 224 | col = NA) + 225 | scale_fill_viridis_c(direction = -1, option = "plasma")+ 226 | theme_void()+ 227 | theme(legend.position = "") 228 | ``` 229 | 230 | 231 | 232 | - A single polygon of the spiral can be made with `st_union()`. The 233 | boundary linestring could also be returned afterwards by using 234 | `st_boundary()` 235 | 236 | ``` r 237 | ggplot() + geom_sf(data = intersections |> st_union(), col = 1, fill = "red") 238 | last_plot() + coord_sf(xlim = c(0.4, 0.6), ylim = c(0.4, 0.6)) 239 | ``` 240 | 241 | 242 | 243 | ## Create a function 244 | 245 | - Put together a function that runs the code above, with some small 246 | additions 247 | - The ability to invert the image 248 | - Control the outer spiral radius by a factor `spiral_r1_f` where 249 | a value of 1 takes it to the edge of the image 250 | - Parameterise the spiral max thickness with a factor `thick_f` 251 | where a value of 1 should make the spiral touch in the thickest 252 | areas 253 | - Note that this function also calls the `spiral_cords()` function 254 | defined above 255 | 256 | ``` r 257 | spiral_image <- 258 | function( 259 | img, 260 | invert = FALSE, 261 | size = 300, 262 | n_shades = 16, 263 | spiral_points = 5000, 264 | spiral_turns = 50, 265 | spiral_r0 = 0, 266 | spiral_r1_f = 1, 267 | thin = 0.00025, 268 | thick_f = 0.95, 269 | spiral_offset_angle = 0, 270 | col_line = "black", 271 | col_bg = "white"){ 272 | 273 | # Read image -------------------------------------------------------------- 274 | if(class(img) == "magick-image"){i <- img} else {i <- magick::image_read(img)} 275 | 276 | # Process image to sf polygon 277 | i_sf <- 278 | i |> 279 | magick::image_resize(paste0(size,"x",size,"^")) |> 280 | magick::image_crop(geometry = paste0(size,"x",size), gravity = "center") |> 281 | magick::image_convert(type = "grayscale") |> 282 | magick::image_quantize(max = n_shades, dither=FALSE) |> 283 | magick::image_flip() |> 284 | magick::image_raster() |> 285 | dplyr::mutate( 286 | col2rgb(col) |> t() |> as_tibble(), 287 | col = scales::rescale(red, to = if(invert){c(0,1)}else{c(1, 0)})) |> 288 | dplyr::select(-green, -blue, -red) |> 289 | stars::st_as_stars() |> 290 | sf::st_as_sf(as_points = FALSE, merge = TRUE) |> 291 | sf::st_make_valid() |> 292 | sf::st_set_agr("constant") |> 293 | sf::st_normalize() 294 | 295 | # Generate spiral ---------------------------------------------------------- 296 | spiral <- 297 | spiral_coords( 298 | xo = 0.5, 299 | yo = 0.5, 300 | n_points = spiral_points, 301 | n_turns = spiral_turns, 302 | r0 = spiral_r0, 303 | r1 = 0.5 * spiral_r1_f, 304 | offset_angle = spiral_offset_angle) |> 305 | as.matrix() |> 306 | sf::st_linestring() 307 | 308 | # Compute the thick value 309 | thick <- ((((0.5*spiral_r1_f) - spiral_r0)/spiral_turns)/2)*thick_f 310 | 311 | intersections <- 312 | sf::st_intersection(i_sf, spiral) |> 313 | dplyr::mutate(n = scales::rescale(col, to=c(thin, thick))) |> 314 | dplyr::mutate(geometry = sf::st_buffer(geometry, n, endCapStyle = "ROUND")) |> 315 | sf::st_union() 316 | 317 | ggplot2::ggplot() + 318 | ggplot2::geom_sf(data = intersections, fill = col_line, col = NA)+ 319 | ggplot2::theme_void()+ 320 | ggplot2::theme(panel.background = ggplot2::element_rect(fill = col_bg, colour = NA))+ 321 | ggplot2::scale_x_continuous(limits = c(0,1))+ 322 | ggplot2::scale_y_continuous(limits = c(0,1)) 323 | } 324 | ``` 325 | 326 | - Some examples 327 | 328 | ``` r 329 | spiral_image(i) 330 | spiral_image(i, invert = TRUE) 331 | spiral_image(i, col_bg = "grey20", col_line = "hotpink", invert = TRUE) 332 | spiral_image(i, spiral_r0 = 0.1) 333 | ``` 334 | 335 | 336 | 337 | - Some interesting effects can be achieved by setting the 338 | `spiral_turns` to be specific fractions of `spiral_points` 339 | 340 | ``` r 341 | spiral_image(i, spiral_points = 150, spiral_turns = 50, thick_f = 0.5) 342 | 343 | spiral_image(i, spiral_points = 200, spiral_turns = 50, thick_f = 0.5) 344 | spiral_image(i, spiral_points = 200, spiral_turns = 50, thick_f = 0.5, spiral_offset_angle = 45) 345 | 346 | spiral_image( 347 | i, 348 | spiral_points = 250, 349 | spiral_turns = 50, 350 | invert = TRUE, 351 | col_bg = "grey10", 352 | col_line = "cyan", 353 | thick_f = 0.5) 354 | ``` 355 | 356 | 357 | 358 | ## Double image on spiral 359 | 360 | - One last experiment 361 | - Using the ability in `st_buffer()` to buffer only one side of the 362 | spiral line, it’s possible to render two images on the same spiral 363 | - Here I read a second image of audrey 364 | 365 | ``` r 366 | i2 <- 367 | magick::image_read('https://images.photowall.com/products/59144/audrey-hepburn-in-breakfast-at-tiffanys-1.jpg?h=699&q=85') |> 368 | magick::image_resize("300x") 369 | 370 | i2 371 | ``` 372 | 373 | 374 | 375 | - Process the image as before 376 | - When it comes to the intersections, I buffer one with positive 377 | values and one with negative values and set `singleSide = TRUE` 378 | - It doesn’t look like the `endCapStyle = "ROUND"` works for me 379 | when buffering on a single side… 380 | - Then visualise both sets of intersections with different colours 381 | - The fact that we normalised the `{sf}` polygon images to be between 382 | 0 and 1 makes this fairly trivial 383 | 384 | ``` r 385 | i2_sf <- 386 | i2 |> 387 | image_resize(paste0("300x300^")) |> 388 | image_crop(geometry = paste0("300x300"), gravity = "center") |> 389 | image_convert(type = "grayscale") |> 390 | image_quantize(max = 16, dither=FALSE) |> 391 | image_flip() |> 392 | image_raster() |> 393 | mutate( 394 | col2rgb(col) |> t() |> as_tibble(), 395 | col = scales::rescale(red, to = c(1, 0))) |> 396 | select(-green, -blue, -red) |> 397 | stars::st_as_stars() |> 398 | st_as_sf(as_points = FALSE, merge = TRUE) |> 399 | st_set_agr("constant") |> 400 | st_normalize() |> 401 | st_make_valid() 402 | 403 | intersections_1 <- 404 | st_intersection(i_sf, spiral) |> 405 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 406 | mutate(geometry = st_buffer(geometry, n, singleSide = TRUE)) 407 | 408 | intersections_2 <- 409 | st_intersection(i2_sf, spiral) |> 410 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 411 | filter(st_geometry_type(geometry) != "POINT") |> # need to remove these for a negative buffer(?) 412 | mutate(geometry = st_buffer(geometry, -n, singleSide = TRUE)) 413 | 414 | ggplot() + 415 | geom_sf(data = intersections_1, fill = 2, col = NA)+ 416 | geom_sf(data = intersections_2, fill = 4, col = NA) 417 | ``` 418 | 419 | ![](README_files/figure-gfm/unnamed-chunk-18-1.png) 420 | 421 | ``` r 422 | last_plot() + coord_sf(xlim = c(0.3, 0.7), ylim = c(0.3, 0.7)) 423 | ``` 424 | 425 | ![](README_files/figure-gfm/unnamed-chunk-18-2.png) 426 | 427 | ## Beyond spirals 428 | 429 | - Of course this method lends itself to any shapes being used (not 430 | just spirals) 431 | 432 | ### Grids 433 | 434 | - Here’s a very quick look at using a grid of squares or hexagons 435 | instead using `st_make_grid()` 436 | - Just using the same values of `thin` and `thick` as before. This 437 | should be changed 438 | 439 | ``` r 440 | square_grid <- st_make_grid(i_sf, n = 50, square = TRUE) |> st_boundary() |> st_union() 441 | 442 | intersections <- 443 | st_intersection(i_sf, square_grid) |> 444 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 445 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 446 | 447 | ggplot() + geom_sf(data = intersections, fill = 1, col = NA) 448 | ``` 449 | 450 | ![](README_files/figure-gfm/unnamed-chunk-19-1.png) 451 | 452 | ``` r 453 | hex_grid <- st_make_grid(i_sf, n = 50, square = FALSE) |> st_boundary() |> st_union() 454 | 455 | intersections <- 456 | st_intersection(i_sf, hex_grid) |> 457 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 458 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 459 | 460 | ggplot() + geom_sf(data = intersections, fill = 1, col = NA) 461 | ``` 462 | 463 | ![](README_files/figure-gfm/unnamed-chunk-20-1.png) 464 | 465 | ### Flow field 466 | 467 | - I’ve modified some code from my [flow field art(?) 468 | repo](https://github.com/cj-holmes/flow-field-art) to create a set 469 | of LINESTRINGs across a flow field 470 | - This is just a quick proof of concept 471 | 472 | ``` r 473 | set.seed(4) 474 | 475 | # Set dimensions for noise generation 476 | x_side <- 400 477 | y_side <- 400 478 | 479 | # Create a noise field matrix with the {ambient} package 480 | m <- 481 | ambient::noise_simplex( 482 | c(y_side, x_side), 483 | frequency = 0.0003, 484 | octaves = 1, 485 | pertubation = "normal", 486 | pertubation_amplitude = 2, 487 | fractal = 'billow') |> 488 | scales::rescale(c(-90, 90)) # scale noise values to angles in degrees (-90 to 90) 489 | 490 | # Get the coords of flow line across the angle matrix 491 | ff_polys <- function( 492 | x_start, 493 | y_start, 494 | step_length, 495 | n_steps, 496 | angle_matrix){ 497 | 498 | # Initialise vectors with the starting x and y values filled with NAs 499 | out_x <- c(x_start, rep(NA, n_steps)) 500 | out_y <- c(y_start, rep(NA, n_steps)) 501 | 502 | # If the starting point is outside the angle_matrix dimensions, return NULL 503 | if(x_start > ncol(angle_matrix) | 504 | x_start < 1 | 505 | y_start > nrow(angle_matrix) | 506 | y_start < 1){ 507 | return(NULL) 508 | } 509 | 510 | # Loop through each step as we travel across the angle matrix 511 | for(i in 1:n_steps){ 512 | 513 | # Get the angle of the nearest flow field point where we are for this iteration 514 | a <- angle_matrix[round(out_y[i]), round(out_x[i])] 515 | 516 | # Compute how far to move in x and y for the given angle and step_length 517 | step_x <- cos(a*(pi/180))*step_length 518 | step_y <- sin(a*(pi/180))*step_length 519 | 520 | # Add the distance in x and y to the current location 521 | next_x <- out_x[i] + step_x 522 | next_y <- out_y[i] + step_y 523 | 524 | # If the next point in the path sits outside the angle matrix, stop iterating along the path 525 | if(next_x > ncol(angle_matrix) | 526 | next_x < 1 | 527 | next_y > nrow(angle_matrix) | 528 | next_y < 1){ 529 | break 530 | } 531 | 532 | # Append the new x and y location to the output 533 | # (ready to be used as the starting point for the next step iteration) 534 | out_x[i+1] <- next_x 535 | out_y[i+1] <- next_y 536 | } 537 | 538 | # Return tibble of the x, y, paths 539 | tibble(x = out_x, y = out_y) |> filter(!is.na(x), !is.na(y)) 540 | } 541 | 542 | # Define number of points for flow lines to start at 543 | n <- 800 544 | 545 | # Re assign thick and thin 546 | thin <- 0.0001 547 | thick <- 0.0025 548 | 549 | # Compute the flow line LINESTRINGs 550 | ff <- 551 | tibble( 552 | x_start = runif(n, 1, ncol(m)), 553 | y_start = runif(n, 1, nrow(m))) |> 554 | mutate( 555 | id = row_number(), 556 | step_length = 1, 557 | n_steps = 400) |> 558 | mutate( 559 | paths = pmap( 560 | .l = list(x_start = x_start, 561 | y_start = y_start, 562 | step_length = step_length, 563 | n_steps = n_steps), 564 | .f = ff_polys, 565 | angle_matrix = m)) |> 566 | unnest(cols=paths) |> 567 | st_as_sf(coords = c("x", "y")) |> 568 | group_by(id) |> 569 | summarise() |> 570 | mutate(type = st_geometry_type(geometry)) |> 571 | filter(type == "MULTIPOINT") |> 572 | st_cast("LINESTRING") |> 573 | st_union() |> 574 | st_normalize() 575 | 576 | # Compute intersections and plot 577 | intersections <- 578 | st_intersection(i_sf, ff) |> 579 | mutate(n = scales::rescale(col, to=c(thin, thick))) |> 580 | mutate(geometry = st_buffer(geometry, n, endCapStyle = "ROUND")) 581 | 582 | ggplot() + geom_sf(data = intersections, fill = 1, col = NA) 583 | ``` 584 | 585 | ![](README_files/figure-gfm/unnamed-chunk-21-1.png) 586 | --------------------------------------------------------------------------------