diff --git a/.gitignore b/.gitignore index f43a00f..da3194c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,18 @@ WarholBea.png logomovie.mp4 logoweb.mp4 sets +1_CreateLogo.R +1_CreateSet.R +1_CreateSet_Penguin.R +1_CreateSet_Rocket.R +1_CreateSet_Trex.R +housemovie.mp4 +houseweb.mp4 +penguinmovie.mp4 +penguinweb.mp4 +rocketmovie.mp4 +rocketmovie2.mp4 +rocketweb.mp4 +rocketweb2.mp4 +trexmovie.mp4 +trexweb.mp4 diff --git a/NAMESPACE b/NAMESPACE index c8e066b..712b4c8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,11 +1,15 @@ # Generated by roxygen2: do not edit by hand export("%>%") +export(collect_3d) export(collect_bricks) export(convert_to_match_color) +export(display_3d) +export(display_pieces) export(display_set) export(generate_instructions) export(image_to_bricks) export(legoize) export(scale_image) +export(table_pieces) importFrom(magrittr,"%>%") diff --git a/R/image_to_3d.R b/R/image_to_3d.R new file mode 100644 index 0000000..aca6cd4 --- /dev/null +++ b/R/image_to_3d.R @@ -0,0 +1,131 @@ +#' Convert image output from scale_image() to bricks +#' +#' @param image_list List output from collect_bricks() or image_to_bricks(). Contains an element \code{Img_lego}. +#' @param mosaic_height Maximum height of 3D mosiacs in LEGO plates. 3 plates = 1 brick. This is also the maximum # of distinct elevations. +#' @param highest_el Brick height is determined by brightness of color. Use \code{highest_el = 'dark'} for darkest bricks to have \code{mosaic_height}. +#' @return A list with elements \code{threed_elevation} and \code{threed_hillshade} to created 3D mosiacs with the \code{rayshader} package. +#' @export +#' +collect_3d <- function(image_list, mosaic_height = 6, highest_el = "light"){ + + #Get previous data + in_list <- image_list + + if(in_list$mosaic_type != "flat")stop("3D mosaics can only be generated with 'flat' mosaics. Set this input in the 'collect_bricks' function.") + + BrickIDs <- in_list$ID_bricks + img_lego <- in_list$Img_lego + + #Number of 'pixels' on a side of a single-stud brick. I think this should be fixed for now + ex_size <- 15 + + lego_expand <- img_lego %>% + dplyr::select(x, y, Lego_name, Lego_color) %>% + dplyr::mutate(stud_id = dplyr::row_number()) + + lego_expand2 <- expand.grid(x = (min(lego_expand$x)*ex_size):(max(lego_expand$x+1)*ex_size), + y = (min(lego_expand$y)*ex_size):(max(lego_expand$y+1)*ex_size)) %>% + dplyr::mutate(x_comp = x %/% ex_size, + y_comp = y %/% ex_size) %>% + dplyr::left_join(lego_expand %>% dplyr::rename(x_comp = x, y_comp = y), + by = c("x_comp", "y_comp")) %>% + dplyr::left_join(BrickIDs %>% dplyr::select(brick_id, x_comp = x, y_comp = y), + by = c("x_comp", "y_comp")) %>% + dplyr::select(-x_comp, -y_comp) %>% + dplyr::left_join(lego_colors %>% dplyr::select(Lego_name = Color, R_lego, G_lego, B_lego), + by = "Lego_name") %>% + dplyr::do( + if(highest_el == "dark"){ + dplyr::mutate(., elevation = (1-((R_lego + G_lego + B_lego )/3)) * 1000) + } else { + dplyr::mutate(., elevation = ( ((R_lego + G_lego + B_lego )/3)) * 1000) + } + ) %>% + #Round elevation to nearest 1/height + dplyr::mutate(elevation = as.numeric(as.factor(cut(elevation, mosaic_height)))) %>% + dplyr::mutate(y = max(y)-y) %>% + dplyr::filter(!is.na(elevation)) %>% + #Calculate stud placement... radius of 1/3 and height of 0.5 plate + dplyr::group_by(stud_id) %>% + dplyr::mutate(x_mid = median(x), y_mid = median(y), + stud = ((x-x_mid)^2 + (y-y_mid)^2)^(1/2) < ex_size/3) %>% + dplyr::ungroup() %>% + dplyr::mutate(elevation = ifelse(stud, elevation+0.5, elevation)) %>% + dplyr::mutate_at(dplyr::vars(R_lego, G_lego, B_lego), dplyr::funs(ifelse(stud, .-0.1, .))) %>% + dplyr::mutate_at(dplyr::vars(R_lego, G_lego, B_lego), dplyr::funs(ifelse(. < 0, 0, .))) + + edges <- dplyr::bind_rows(list( + lego_expand2 %>% dplyr::filter(x == min(x)) %>% dplyr::mutate(x = x-1), + lego_expand2 %>% dplyr::filter(x == max(x)) %>% dplyr::mutate(x = x+1), + lego_expand2 %>% dplyr::filter(y == min(y)) %>% dplyr::mutate(y = y-1), + lego_expand2 %>% dplyr::filter(y == max(y)) %>% dplyr::mutate(y = y+1) + )) %>% + dplyr:: mutate(R_lego = 1, G_lego = 1, B_lego = 1, + elevation = 0, + brick_id = NA) + + #Elevation Matrix + lego_elmat <- lego_expand2 %>% + dplyr::bind_rows(edges) %>% + dplyr::select(x, y, elevation) %>% + tidyr::spread(y, elevation) %>% + dplyr::select(-x) %>% + as.matrix() + + #Hillshade matrix + lego_hillshade_m <- array(dim = c(length(unique(lego_expand2$y)), + length(unique(lego_expand2$x)), + 3)) + + lego_expand_color <- lego_expand2 %>% + dplyr::group_by(brick_id) %>% + #This darkens the edge of each brick, to look like they are separated + dplyr::mutate_at(dplyr::vars(R_lego, G_lego, B_lego), + dplyr::funs(ifelse((x == min(x) | y == min(y) | x == max(x) | y == max(y)), .*0.75, .))) %>% + dplyr::ungroup() + + lego_hillshade_m[,,1] <- lego_expand_color %>% + dplyr::select(x, y, R_lego) %>% + tidyr::spread(x, R_lego) %>% + dplyr::select(-y) %>% + as.matrix() + + lego_hillshade_m[,,2] <- lego_expand_color %>% + dplyr::select(x, y, G_lego) %>% + tidyr::spread(x, G_lego) %>% + dplyr::select(-y) %>% + as.matrix() + + lego_hillshade_m[,,3] <- lego_expand_color %>% + dplyr::select(x, y, B_lego) %>% + tidyr::spread(x, B_lego) %>% + dplyr::select(-y) %>% + as.matrix() + + #Return + in_list[["threed_elevation"]] <- lego_elmat + in_list[["threed_hillshade"]] <- lego_hillshade_m + + return(in_list) + +} + +#' brickr wrapper for rayshader::plot_3d() to display 3D mosaics. Requires rayshader. +#' +#' @param image_list List output from collect_3d(). Contains element \code{threed_elevation} and \code{threed_hillshade}. +#' @param solidcolor Hex color of mosaic base. Only renders on bottom. +#' @param ... All other inputs from rayshader::plot_3d() EXCEPT \code{hillshade}, \code{soliddepth}, and \code{zscale}. +#' @return 3D mosaic rendered in the 'rgl' package. +#' @export + +display_3d <- function(image_list, solidcolor = "#a3a2a4", ...){ + #Requires Rayshader + if (!requireNamespace("rayshader", quietly = TRUE)) { + stop("Package \"rayshader\" needed for this function to work. Please install it.", + call. = FALSE) + } + + image_list$`threed_hillshade`%>% + rayshader::plot_3d(image_list$`threed_elevation`, zscale=0.125, + solidcolor=solidcolor, ...) +} diff --git a/R/piece_count.R b/R/piece_count.R new file mode 100644 index 0000000..1e51172 --- /dev/null +++ b/R/piece_count.R @@ -0,0 +1,98 @@ +#' Generate required bricks as a data frame. +#' +#' @param image_list List output from collect_bricks() or image_to_bricks(). Contains an element \code{Img_lego}. +#' @return Data frame of piece counts by LEGO color name and size. +#' @export +#' + +table_pieces <- function(image_list){ + pcs <- image_list$pieces + + pcs %>% + dplyr::select(-Lego_color) %>% + tidyr::spread(Brick_size, n, fill = 0) %>% + dplyr::rename(`LEGO Brick Color` = Lego_name) +} + +#' Graphically display required bricks. +#' +#' @param image_list List output from collect_bricks() or image_to_bricks(). Contains an element \code{Img_lego}. +#' @return Plot object of required bricks by color and size. +#' @export +#' + +display_pieces <- function(image_list){ + in_list <- image_list + pcs <- in_list$pieces + + if(in_list$mosaic_type == "flat"){ + pcs_coords <- dplyr::tibble( + Brick_size = c("1 x 1", "2 x 1", "3 x 1", "4 x 1", "2 x 2", "4 x 2"), + xmin = c(0, 0, 0, 0, 6, 6), + xmax = c(1, 2, 3, 4, 8, 8), + ymin = c(0, 2, 4, 6, 0, 3), + ymax = c(1, 3, 5, 7, 2, 7) + ) + } else { + pcs_coords <- dplyr::tibble( + Brick_size = c("1 x 2", "2 x 2", "3 x 2", "4 x 2"), + xmin = c(0, 5, 5, 0), + xmax = c(2, 7, 7, 2), + ymin = c(0, 0, 3, 2), + ymax = c(1, 2, 6, 6) + ) + } + #This function creates nodes in each brick for stud placement + pcs_coords <- pcs_coords %>% + dplyr::mutate(studs = purrr::pmap(list(xmin, xmax, ymin, ymax), function(a, b, c, d){ + expand.grid(x=seq(a+0.5, b-0.5, by=1), + y=seq(c+0.5, d-0.5, by=1)) + })) + + pcs2 <- pcs %>% + dplyr::arrange(Lego_color) %>% + dplyr::mutate(Lego_name = factor(Lego_name, + levels = c("Black", + unique(Lego_name)[!(unique(Lego_name) %in% c("Black", "White"))], + "White"))) %>% + dplyr::left_join(pcs_coords, by = "Brick_size") + + if(in_list$mosaic_type == "flat"){ + coord_xlim <- c(-0.5, 10) + facet_cols <- 5 + } else { + coord_xlim <- c(-0.5, 9) + facet_cols <- 6 + } + + pcs2 %>% + ggplot2::ggplot() + + ggplot2::geom_rect(ggplot2::aes(xmin=xmin, xmax=xmax, ymin=-ymin, ymax=-ymax, + fill = Lego_color), color = "#333333")+ + ggplot2::scale_fill_identity() + + ggplot2::geom_point(data = pcs2 %>% tidyr::unnest(studs), + ggplot2::aes(x=x, y=-y), + color = "#cccccc", alpha = 0.25, + shape = 1, size = 2) + + ggplot2::geom_text( + ggplot2::aes(x = xmax + 0.25, y = -(ymin+ymax)/2, label = paste0("x", n)), + hjust = 0, vjust = 0.5, size = 3.5) + + ggplot2::coord_fixed(xlim = coord_xlim) + + ggplot2::labs(title = (if(in_list$mosaic_type == "stacked"){ + "Suggested LEGO Bricks" + }else{"Suggested LEGO Plates"}), + caption = (if(in_list$mosaic_type == "stacked"){ + "Mosaic is 2-bricks deep. Can substitute 2-stud bricks for 1-stud alternatives for a thinner mosaic."}else{""}) + ) + + ggplot2::facet_wrap(~Lego_name, ncol=facet_cols) + + ggplot2::theme_minimal() + + ggplot2::theme( panel.background = ggplot2::element_rect(fill = "#7EC0EE"), + strip.background = ggplot2::element_rect(fill = "#F7F18D"), + strip.text = ggplot2::element_text(color = "#333333", face = "bold"), + axis.line = ggplot2::element_blank(), + axis.title.x = ggplot2::element_blank(), + axis.text.x = ggplot2::element_blank(), + axis.title.y = ggplot2::element_blank(), + axis.text.y = ggplot2::element_blank(), + panel.grid = ggplot2::element_blank()) +} \ No newline at end of file diff --git a/man/collect_3d.Rd b/man/collect_3d.Rd new file mode 100644 index 0000000..84a9a43 --- /dev/null +++ b/man/collect_3d.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/image_to_3d.R +\name{collect_3d} +\alias{collect_3d} +\title{Convert image output from scale_image() to bricks} +\usage{ +collect_3d(image_list, mosaic_height = 6, highest_el = "light") +} +\arguments{ +\item{image_list}{List output from collect_bricks() or image_to_bricks(). Contains an element \code{Img_lego}.} + +\item{mosaic_height}{Maximum height of 3D mosiacs in LEGO plates. 3 plates = 1 brick. This is also the maximum # of distinct elevations.} + +\item{highest_el}{Brick height is determined by brightness of color. Use \code{highest_el = 'dark'} for darkest bricks to have \code{mosaic_height}.} +} +\value{ +A list with elements \code{threed_elevation} and \code{threed_hillshade} to created 3D mosiacs with the \code{rayshader} package. +} +\description{ +Convert image output from scale_image() to bricks +} diff --git a/man/display_3d.Rd b/man/display_3d.Rd new file mode 100644 index 0000000..79fb26b --- /dev/null +++ b/man/display_3d.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/image_to_3d.R +\name{display_3d} +\alias{display_3d} +\title{brickr wrapper for rayshader::plot_3d() to display 3D mosaics. Requires rayshader.} +\usage{ +display_3d(image_list, solidcolor = "#a3a2a4", ...) +} +\arguments{ +\item{image_list}{List output from collect_3d(). Contains element \code{threed_elevation} and \code{threed_hillshade}.} + +\item{solidcolor}{Hex color of mosaic base. Only renders on bottom.} + +\item{...}{All other inputs from rayshader::plot_3d() EXCEPT \code{hillshade}, \code{soliddepth}, and \code{zscale}.} +} +\value{ +3D mosaic rendered in the 'rgl' package. +} +\description{ +brickr wrapper for rayshader::plot_3d() to display 3D mosaics. Requires rayshader. +} diff --git a/man/display_pieces.Rd b/man/display_pieces.Rd new file mode 100644 index 0000000..ad00f78 --- /dev/null +++ b/man/display_pieces.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/piece_count.R +\name{display_pieces} +\alias{display_pieces} +\title{Graphically display required bricks.} +\usage{ +display_pieces(image_list) +} +\arguments{ +\item{image_list}{List output from collect_bricks() or image_to_bricks(). Contains an element \code{Img_lego}.} +} +\value{ +Plot object of required bricks by color and size. +} +\description{ +Graphically display required bricks. +} diff --git a/man/table_pieces.Rd b/man/table_pieces.Rd new file mode 100644 index 0000000..4432d16 --- /dev/null +++ b/man/table_pieces.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/piece_count.R +\name{table_pieces} +\alias{table_pieces} +\title{Generate required bricks as a data frame.} +\usage{ +table_pieces(image_list) +} +\arguments{ +\item{image_list}{List output from collect_bricks() or image_to_bricks(). Contains an element \code{Img_lego}.} +} +\value{ +Data frame of piece counts by LEGO color name and size. +} +\description{ +Generate required bricks as a data frame. +}