├── LICENSE ├── man ├── figures │ ├── logo.png │ ├── README-cut-1.png │ ├── README-image.png │ ├── README-deform-1.png │ ├── README-square-1.png │ ├── README-round_plot-1.png │ └── README-interpolate-1.png ├── uniform_scaling.Rd ├── normalize_vectors.Rd ├── minmax_scaling.Rd ├── compute_polygon_centroid.Rd ├── compute_polygon_perimeter.Rd ├── compute_polygon_area.Rd ├── rounded_rectangle.Rd ├── generate_random_polygon.Rd ├── rounded_regular_polygon.Rd ├── compute_regular_polygons.Rd ├── cut_polygons.Rd ├── rotate_polygon.Rd ├── transition_between_polygons.Rd ├── image_from_function.Rd ├── round_polygon_corners.Rd ├── interpolate_polygon.Rd └── deform_polygon.Rd ├── R ├── artKIT-package.R ├── RcppExports.R ├── uniform-scaling.R ├── minmax-scaling.R ├── normalize-vectors.R ├── corner-radius-functions.R ├── compute-polygon-perimeter.R ├── generate_random_polygon.R ├── compute-polygon-area.R ├── compute-polygon-centroid.R ├── rounded-regular-polygon.R ├── rounded-rectangle.R ├── utilities.R ├── cut-polygons.R ├── rotate-polygon.R ├── compute-regular-polygons.R ├── transition-between-polygons.R ├── image-from-function.R ├── interpolate-polygon.R ├── deform-polygon.R └── round-polygon-corners.R ├── NAMESPACE ├── DESCRIPTION ├── src ├── RcppExports.cpp └── polygoncutting.cpp └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2021 2 | COPYRIGHT HOLDER: Mathias Isaksen 3 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathiasisaksen/artKIT/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /man/figures/README-cut-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathiasisaksen/artKIT/HEAD/man/figures/README-cut-1.png -------------------------------------------------------------------------------- /man/figures/README-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathiasisaksen/artKIT/HEAD/man/figures/README-image.png -------------------------------------------------------------------------------- /R/artKIT-package.R: -------------------------------------------------------------------------------- 1 | #' @useDynLib artKIT, .registration = TRUE 2 | #' @importFrom Rcpp sourceCpp 3 | NULL 4 | #> NULL 5 | -------------------------------------------------------------------------------- /man/figures/README-deform-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathiasisaksen/artKIT/HEAD/man/figures/README-deform-1.png -------------------------------------------------------------------------------- /man/figures/README-square-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathiasisaksen/artKIT/HEAD/man/figures/README-square-1.png -------------------------------------------------------------------------------- /man/figures/README-round_plot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathiasisaksen/artKIT/HEAD/man/figures/README-round_plot-1.png -------------------------------------------------------------------------------- /man/figures/README-interpolate-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathiasisaksen/artKIT/HEAD/man/figures/README-interpolate-1.png -------------------------------------------------------------------------------- /R/RcppExports.R: -------------------------------------------------------------------------------- 1 | # Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | cutEveryPolygonRcpp <- function(x, y, group, numberOfIterations = 1L, useCentroid = TRUE) { 5 | .Call(`_artKIT_cutEveryPolygonRcpp`, x, y, group, numberOfIterations, useCentroid) 6 | } 7 | 8 | cutEveryPolygonReturnAllRcpp <- function(x, y, group, numberOfIterations = 1L, useCentroid = TRUE) { 9 | .Call(`_artKIT_cutEveryPolygonReturnAllRcpp`, x, y, group, numberOfIterations, useCentroid) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(compute_polygon_area) 4 | export(compute_polygon_centroid) 5 | export(compute_polygon_perimeter) 6 | export(compute_regular_polygons) 7 | export(cut_polygons) 8 | export(deform_polygon) 9 | export(generate_random_polygon) 10 | export(image_from_function) 11 | export(interpolate_polygon) 12 | export(minmax_scaling) 13 | export(normalize_vectors) 14 | export(rotate_polygon) 15 | export(round_polygon_corners) 16 | export(rounded_rectangle) 17 | export(rounded_regular_polygon) 18 | export(transition_between_polygons) 19 | export(uniform_scaling) 20 | importFrom(Rcpp,sourceCpp) 21 | useDynLib(artKIT, .registration = TRUE) 22 | -------------------------------------------------------------------------------- /R/uniform-scaling.R: -------------------------------------------------------------------------------- 1 | #' Transform values to be uniformly distributed 2 | #' 3 | #' Applies a rescaling that transforms the values into being uniformly 4 | #' distributed on the interval [0, 1]. 5 | #' 6 | #' @param values A numeric vector of values 7 | #' 8 | #' @return A numeric vector that contains the rescaled values 9 | #' 10 | #' @examples 11 | #' set.seed(123) 12 | #' # Generate values from normal distribution 13 | #' original.values = rnorm(100, -4, 5) 14 | #' # The values lie between approximately -15.5 and 7 15 | #' hist(original.values) 16 | #' # Rescale values uniformly to the interval [0, 1] 17 | #' uniform.values = uniform_scaling(original.values) 18 | #' hist(uniform.values) 19 | #' 20 | #' @export 21 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 22 | uniform_scaling = function(values) { 23 | ecdf.function = stats::ecdf(values) 24 | rescaled.values = ecdf.function(values) 25 | return(rescaled.values) 26 | } 27 | -------------------------------------------------------------------------------- /man/uniform_scaling.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/uniform-scaling.R 3 | \name{uniform_scaling} 4 | \alias{uniform_scaling} 5 | \title{Transform values to be uniformly distributed} 6 | \usage{ 7 | uniform_scaling(values) 8 | } 9 | \arguments{ 10 | \item{values}{A numeric vector of values} 11 | } 12 | \value{ 13 | A numeric vector that contains the rescaled values 14 | } 15 | \description{ 16 | Applies a rescaling that transforms the values into being uniformly 17 | distributed on the interval [0, 1]. 18 | } 19 | \examples{ 20 | set.seed(123) 21 | # Generate values from normal distribution 22 | original.values = rnorm(100, -4, 5) 23 | # The values lie between approximately -15.5 and 7 24 | hist(original.values) 25 | # Rescale values uniformly to the interval [0, 1] 26 | uniform.values = uniform_scaling(original.values) 27 | hist(uniform.values) 28 | 29 | } 30 | \author{ 31 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 32 | } 33 | -------------------------------------------------------------------------------- /man/normalize_vectors.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/normalize-vectors.R 3 | \name{normalize_vectors} 4 | \alias{normalize_vectors} 5 | \title{Normalize vectors to have length 1} 6 | \usage{ 7 | normalize_vectors(vectors) 8 | } 9 | \arguments{ 10 | \item{vectors}{A n x d matrix or data frame that contains one vector per row, 11 | and one column per dimension.} 12 | } 13 | \value{ 14 | The return value is of the same type as \code{vectors}, and contains 15 | the normalized vectors. 16 | } 17 | \description{ 18 | Takes in one or more vectors and normalizes their lengths to 1. 19 | } 20 | \examples{ 21 | set.seed(123) 22 | # Generate random 2D vectors 23 | vectors = matrix(runif(200, -1, 1), ncol = 2) 24 | # The lengths are varied 25 | summary(sqrt(rowSums(vectors^2))) 26 | # Normalize vectors 27 | normalized.vectors = normalize_vectors(vectors) 28 | # The lengths are all equal to 1 29 | summary(sqrt(rowSums(normalized.vectors^2))) 30 | 31 | } 32 | \author{ 33 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 34 | } 35 | -------------------------------------------------------------------------------- /man/minmax_scaling.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/minmax-scaling.R 3 | \name{minmax_scaling} 4 | \alias{minmax_scaling} 5 | \title{Rescale values using min-max scaling} 6 | \usage{ 7 | minmax_scaling(values, lower = 0, upper = 1) 8 | } 9 | \arguments{ 10 | \item{values}{A numeric vector of values} 11 | 12 | \item{lower, upper}{The lower and upper bounds of the range that the values 13 | are mapped onto} 14 | } 15 | \value{ 16 | A numeric vector that contains the rescaled values 17 | } 18 | \description{ 19 | Applies a rescaling that linearly transforms the values into a specified 20 | range of values. 21 | } 22 | \examples{ 23 | set.seed(123) 24 | # Generate values from normal distribution 25 | original.values = rnorm(100, -4, 5) 26 | # The values lie between approximately -15.5 and 7 27 | hist(original.values) 28 | # Rescale values to the interval [0, 1] 29 | rescaled.values = minmax_scaling(original.values) 30 | hist(rescaled.values) 31 | 32 | } 33 | \author{ 34 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 35 | } 36 | -------------------------------------------------------------------------------- /R/minmax-scaling.R: -------------------------------------------------------------------------------- 1 | #' Rescale values using min-max scaling 2 | #' 3 | #' Applies a rescaling that linearly transforms the values into a specified 4 | #' range of values. 5 | #' 6 | #' @param values A numeric vector of values 7 | #' @param lower,upper The lower and upper bounds of the range that the values 8 | #' are mapped onto 9 | #' 10 | #' @return A numeric vector that contains the rescaled values 11 | #' 12 | #' @examples 13 | #' set.seed(123) 14 | #' # Generate values from normal distribution 15 | #' original.values = rnorm(100, -4, 5) 16 | #' # The values lie between approximately -15.5 and 7 17 | #' hist(original.values) 18 | #' # Rescale values to the interval [0, 1] 19 | #' rescaled.values = minmax_scaling(original.values) 20 | #' hist(rescaled.values) 21 | #' 22 | #' @export 23 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 24 | minmax_scaling = function(values, lower = 0, upper = 1) { 25 | if (length(values) < 2) { 26 | stop("values must contain at least two elements.") 27 | } 28 | m = min(values) 29 | M = max(values) 30 | rescaled.values = lower + (upper - lower)*(values - m)/(M - m) 31 | return(rescaled.values) 32 | } 33 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: artKIT 2 | Type: Package 3 | Title: Useful functions for creating visual/generative art 4 | Version: 0.1.0 5 | Author: Mathias Isaksen 6 | Maintainer: Mathias Isaksen 7 | Description: This package contains a set of functions that are useful 8 | when creating visual/generative art. At the moment, most of the package 9 | deals with polygons/shapes, exposing functions that allows the user to 10 | do boundary interpolation, corner rounding, deformation and geometric subdivison. 11 | There are also functions for rescaling values, and creating images based on 12 | functions that map pixel locations to colors. 13 | Vectorization is used throughout the package, leading to efficient code. 14 | The package is a work in progress, and will be updated as soon as more 15 | functions are ready. 16 | License: MIT + file LICENSE 17 | BugReports: https://github.com/mathiasisaksen/artKIT/issues 18 | Encoding: UTF-8 19 | LazyData: true 20 | Imports: Rcpp, lpSolve, png, grDevices 21 | Suggests: ggplot2 22 | LinkingTo: Rcpp 23 | RoxygenNote: 7.1.1 24 | NeedsCompilation: yes 25 | Packaged: 2021-07-31 13:46:26 UTC; mathias 26 | -------------------------------------------------------------------------------- /R/normalize-vectors.R: -------------------------------------------------------------------------------- 1 | #' Normalize vectors to have length 1 2 | #' 3 | #' Takes in one or more vectors and normalizes their lengths to 1. 4 | #' 5 | #' @param vectors A n x d matrix or data frame that contains one vector per row, 6 | #' and one column per dimension. 7 | #' 8 | #' @return The return value is of the same type as \code{vectors}, and contains 9 | #' the normalized vectors. 10 | #' 11 | #' @examples 12 | #' set.seed(123) 13 | #' # Generate random 2D vectors 14 | #' vectors = matrix(runif(200, -1, 1), ncol = 2) 15 | #' # The lengths are varied 16 | #' summary(sqrt(rowSums(vectors^2))) 17 | #' # Normalize vectors 18 | #' normalized.vectors = normalize_vectors(vectors) 19 | #' # The lengths are all equal to 1 20 | #' summary(sqrt(rowSums(normalized.vectors^2))) 21 | #' 22 | #' @export 23 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 24 | normalize_vectors = function(vectors) { 25 | if (is.null(dim(vectors))) { 26 | vectors = matrix(vectors, nrow = 1) 27 | } 28 | 29 | lengths = rep(0, nrow(vectors)) 30 | for (i in 1:ncol(vectors)) { 31 | lengths = lengths + vectors[, i]^2 32 | } 33 | lengths = sqrt(lengths) 34 | nonzero.elems = lengths > 0 35 | 36 | result = vectors 37 | result[nonzero.elems, ] = result[nonzero.elems, ] / lengths[nonzero.elems] 38 | return(result) 39 | } 40 | -------------------------------------------------------------------------------- /R/corner-radius-functions.R: -------------------------------------------------------------------------------- 1 | 2 | compute_varying_corner_radius = function(edge.lengths, angles) { 3 | n = length(edge.lengths) 4 | 5 | # Linear programming with solution (l[1],..., l[n], Z). 6 | # Using L = edge.lengths, we need l[i] + l[i+1] <= L[i] and l[i] > Z, where Z 7 | # is a dummy variable that ensures that min(l) is as large as possible. 8 | # The first constraint leads to A.top, while the latter leads to A.bottom. 9 | A.top = diag(1, nrow = n, ncol = n + 1) 10 | A.top[cbind(1:(n - 1), 2:n)] = 1 11 | A.top[n, 1] = 1 12 | A.bottom = diag(-1, nrow = n, ncol = n + 1) 13 | A.bottom[, n + 1] = 1 14 | A = rbind(A.top, A.bottom) 15 | 16 | # Give equal weight to all variables 17 | obj = c(rep(1, n), 1) 18 | #Gives more weight where the angle is big, needs to be explored further 19 | #obj = c(pi - angles/2, 1) 20 | # All inequalities are less-than-or-equal 21 | dir = rep("<=", nrow(A)) 22 | # The RHS of the first constraint is edge.lengths, while the second 23 | # constraint has RHS equal to 0 24 | rhs = c(edge.lengths, rep(0, n)) 25 | 26 | lp.sol = lpSolve::lp("max", obj, A, dir, rhs) 27 | l = lp.sol$solution[1:n] 28 | corner.radius = l*tan(angles / 2) 29 | return(corner.radius) 30 | } 31 | 32 | compute_constant_corner_radius = function(edge.lengths, angles) { 33 | n = length(edge.lengths) 34 | corner.radius = min(edge.lengths / (1/tan(angles/2) + 1/tan(angles[c(2:n, 1)]/2))) 35 | return(rep(corner.radius, n)) 36 | } 37 | -------------------------------------------------------------------------------- /man/compute_polygon_centroid.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/compute-polygon-centroid.R 3 | \name{compute_polygon_centroid} 4 | \alias{compute_polygon_centroid} 5 | \title{Compute polygon centroid} 6 | \usage{ 7 | compute_polygon_centroid(vertex.df) 8 | } 9 | \arguments{ 10 | \item{vertex.df}{A data frame where each row corresponds to the vertex of a polygon. 11 | It must contain the columns \code{x}, \code{y}, and optionally \code{group} (must be integer). 12 | \code{x} and \code{y} specify the coordinates of the vertex, 13 | and \code{group} is used to indicate which polygon the vertex belongs to. 14 | If \code{vertex.df} contains only a single polygon, \code{group} can be omitted.} 15 | } 16 | \value{ 17 | A data frame that contains three columns: 18 | \item{group}{The group number of the polygon} 19 | \item{x, y}{The coordinates of the centroid} 20 | } 21 | \description{ 22 | This function takes in a data frame containing one or more polygons, 23 | and computes the centroid (geometric center) of each polygon. 24 | } 25 | \examples{ 26 | # Example with multiple polygons 27 | vertex.df = compute_regular_polygons( 28 | center = data.frame(x = c(0, 10), y = c(0, 0)), 29 | radius = 1, 30 | num.edges = c(4, 1000) 31 | ) 32 | centroid.df = compute_polygon_centroid(vertex.df) 33 | 34 | # Centroids should be (0, 0) and c(10, 0) 35 | print(centroid.df) 36 | 37 | } 38 | \author{ 39 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 40 | } 41 | -------------------------------------------------------------------------------- /man/compute_polygon_perimeter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/compute-polygon-perimeter.R 3 | \name{compute_polygon_perimeter} 4 | \alias{compute_polygon_perimeter} 5 | \title{Compute polygon perimeter} 6 | \usage{ 7 | compute_polygon_perimeter(vertex.df) 8 | } 9 | \arguments{ 10 | \item{vertex.df}{A data frame where each row corresponds to the vertex of a polygon. 11 | It must contain the columns \code{x}, \code{y}, and optionally \code{group} (must be integer). 12 | \code{x} and \code{y} specify the coordinates of the vertex, 13 | and \code{group} is used to indicate which polygon the vertex belongs to. 14 | If \code{vertex.df} contains only a single polygon, \code{group} can be omitted.} 15 | } 16 | \value{ 17 | A data frame that contains two columns: 18 | \item{group}{The group number of the polygon} 19 | \item{perimeter}{The perimeter of the polygon} 20 | } 21 | \description{ 22 | This function takes in a data frame containing one or more polygons, 23 | and computes the perimeter of each polygon 24 | } 25 | \examples{ 26 | # Example with multiple polygons 27 | vertex.df = compute_regular_polygons( 28 | center = data.frame(x = c(0, 10), y = c(0, 0)), 29 | radius = 1, 30 | num.edges = c(4, 1000) 31 | ) 32 | perimeter.df = compute_polygon_perimeter(vertex.df) 33 | 34 | # The perimeters should be around 4*sqrt(2) and 2*pi 35 | print(perimeter.df) 36 | 37 | } 38 | \author{ 39 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 40 | } 41 | -------------------------------------------------------------------------------- /man/compute_polygon_area.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/compute-polygon-area.R 3 | \name{compute_polygon_area} 4 | \alias{compute_polygon_area} 5 | \title{Compute polygon area} 6 | \usage{ 7 | compute_polygon_area(vertex.df, signed = FALSE) 8 | } 9 | \arguments{ 10 | \item{vertex.df}{A data frame where each row corresponds to the vertex of a polygon. 11 | It must contain the columns \code{x}, \code{y}, and optionally \code{group} (must be integer). 12 | \code{x} and \code{y} specify the coordinates of the vertex, 13 | and \code{group} is used to indicate which polygon the vertex belongs to. 14 | If \code{vertex.df} contains only a single polygon, \code{group} can be omitted.} 15 | 16 | \item{signed}{If true, the signed areas of the polygons are returned (positive for 17 | counter-clockwise oriented polygons, negative for clockwise oriented.)} 18 | } 19 | \value{ 20 | A data frame that contains two columns: 21 | \item{group}{The group number of the polygon} 22 | \item{area}{The area of the polygon} 23 | } 24 | \description{ 25 | This function takes in a data frame containing one or more polygons, 26 | and computes the (signed) area of each polygon. 27 | } 28 | \examples{ 29 | # Example with multiple polygons 30 | vertex.df = compute_regular_polygons( 31 | center = data.frame(x = c(0, 10), y = c(0, 0)), 32 | radius = 1, 33 | num.edges = c(4, 1000) 34 | ) 35 | area.df = compute_polygon_area(vertex.df) 36 | 37 | # Note that the area of polygon 2 is close to pi! 38 | print(area.df) 39 | 40 | } 41 | \author{ 42 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 43 | } 44 | -------------------------------------------------------------------------------- /man/rounded_rectangle.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rounded-rectangle.R 3 | \name{rounded_rectangle} 4 | \alias{rounded_rectangle} 5 | \title{Generate rectangle with rounded corners} 6 | \usage{ 7 | rounded_rectangle( 8 | center = c(0, 0), 9 | width = 1, 10 | height = 1, 11 | rotation = 0, 12 | corner.radius = NULL, 13 | vertices.per.corner = 50 14 | ) 15 | } 16 | \arguments{ 17 | \item{center}{The center/centroid of the shape. Can either be a 1 x 2 data 18 | frame/matrix or a numeric vector length 2.} 19 | 20 | \item{width, height}{The width and height of the rectangle before the corners 21 | are rounded.} 22 | 23 | \item{rotation}{The amount of rotation applied to the shape, in radians.} 24 | 25 | \item{corner.radius}{The radius of the circular arcs. Should be less than min(width, height) / 2.} 26 | 27 | \item{vertices.per.corner}{The number of vertices that each circular arc consists of.} 28 | } 29 | \value{ 30 | A data frame containing the columns \code{x} and \code{y}, where each 31 | row is a vertex in the rounded rectangle. 32 | } 33 | \description{ 34 | This function computes the vertices of a rectangle with rounded corners. 35 | } 36 | \note{ 37 | The result may contain less than \code{4*vertices.per.corner} vertices. 38 | In certain situations, consecutive vertices will end up having the same coordinates. 39 | The duplicates are then removed. 40 | } 41 | \examples{ 42 | # Create a rounded rectangle 43 | vertex.df = rounded_rectangle( 44 | center = c(2, 5), 45 | width = 3, 46 | height = 6, 47 | rotation = pi/4, 48 | corner.radius = 1 49 | ) 50 | 51 | # How the rounded rectangle looks: 52 | library(ggplot2) 53 | ggplot()+ 54 | geom_polygon(data = vertex.df, aes(x = x, y = y))+ 55 | coord_fixed() 56 | 57 | } 58 | \author{ 59 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 60 | } 61 | -------------------------------------------------------------------------------- /man/generate_random_polygon.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/generate_random_polygon.R 3 | \name{generate_random_polygon} 4 | \alias{generate_random_polygon} 5 | \title{Generate random polygon} 6 | \usage{ 7 | generate_random_polygon( 8 | center = c(0, 0), 9 | min.radius = 0.5, 10 | max.radius = 1, 11 | num.vertices = 5, 12 | seed = NULL 13 | ) 14 | } 15 | \arguments{ 16 | \item{center}{The "center" of the polygon, which determines its position. Should 17 | either be a 1 x 2 data frame/matrix or a numeric vector with 2 elements.} 18 | 19 | \item{min.radius, max.radius}{The minimum and maximum bounds used in the uniform 20 | distribution that samples the radius values} 21 | 22 | \item{num.vertices}{The number of vertices that the random polygon should contain} 23 | 24 | \item{seed}{The seed used when sampling the radius value} 25 | } 26 | \value{ 27 | A data frame that contains one row per vertex in the polygon, and the 28 | columns \code{x} and \code{y}. 29 | } 30 | \description{ 31 | This function generates a random polygon. It uses the polar coordinate 32 | representation, where the angles are evenly spaced over the interval [0, 2pi] 33 | and the radiuses are sampled from a uniform distribution. 34 | } 35 | \examples{ 36 | # A random polygon with center (5, 10) and 10 vertices. The distance from 37 | # the center to the vertices is between 4 and 10 38 | vertex.df = generate_random_polygon( 39 | center = c(5, 10), 40 | min.radius = 4, 41 | max.radius = 10, 42 | num.vertices = 10, 43 | seed = 123 44 | ) 45 | 46 | library(ggplot2) 47 | # The center of the polygon is shown in red 48 | ggplot()+ 49 | geom_polygon(data = vertex.df, aes(x = x, y = y))+ 50 | geom_point(aes(x = 5, y = 10), color = "red")+ 51 | coord_fixed() 52 | 53 | } 54 | \author{ 55 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 56 | } 57 | -------------------------------------------------------------------------------- /man/rounded_regular_polygon.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rounded-regular-polygon.R 3 | \name{rounded_regular_polygon} 4 | \alias{rounded_regular_polygon} 5 | \title{Generate regular polygon with rounded corners} 6 | \usage{ 7 | rounded_regular_polygon( 8 | num.edges = 3, 9 | center = c(0, 0), 10 | radius = 1, 11 | rotation = 0, 12 | corner.radius = 0.25, 13 | vertices.per.corner = 50 14 | ) 15 | } 16 | \arguments{ 17 | \item{num.edges}{The number of edges that the regular polygon contains.} 18 | 19 | \item{center}{The center/centroid of the shape. Can either be a 1 x 2 data 20 | frame/matrix or a numeric vector length 2.} 21 | 22 | \item{radius}{The radius of the circumscribed circle of the regular polygon.} 23 | 24 | \item{rotation}{The amount of rotation applied to the shape, in radians.} 25 | 26 | \item{corner.radius}{The radius of the circular arcs.} 27 | 28 | \item{vertices.per.corner}{The number of vertices that each circular arc consists of.} 29 | } 30 | \value{ 31 | A data frame containing the columns \code{x} and \code{y}, where each 32 | row is a vertex in the rounded polygon. 33 | } 34 | \description{ 35 | This function computes the vertices of a regular polygon with rounded corners. 36 | } 37 | \note{ 38 | The result may contain less than \code{num.edges*vertices.per.corner} vertices. 39 | In certain situations, consecutive vertices will end up having the same coordinates. 40 | The duplicates are then removed. 41 | } 42 | \examples{ 43 | # Create a rounded hexagon 44 | vertex.df = rounded_regular_polygon( 45 | num.edges = 6, 46 | center = c(5, 6), 47 | radius = 2, 48 | rotation = pi/4, 49 | corner.radius = 0.5 50 | ) 51 | 52 | # How the rounded hexagon looks: 53 | library(ggplot2) 54 | ggplot()+ 55 | geom_polygon(data = vertex.df, aes(x = x, y = y))+ 56 | coord_fixed() 57 | 58 | } 59 | \author{ 60 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 61 | } 62 | -------------------------------------------------------------------------------- /R/compute-polygon-perimeter.R: -------------------------------------------------------------------------------- 1 | #' Compute polygon perimeter 2 | #' 3 | #' This function takes in a data frame containing one or more polygons, 4 | #' and computes the perimeter of each polygon 5 | #' 6 | #' @inheritParams cut_polygons 7 | #' 8 | #' @return A data frame that contains two columns: 9 | #' \item{group}{The group number of the polygon} 10 | #' \item{perimeter}{The perimeter of the polygon} 11 | #' 12 | #' @examples 13 | #' # Example with multiple polygons 14 | #' vertex.df = compute_regular_polygons( 15 | #' center = data.frame(x = c(0, 10), y = c(0, 0)), 16 | #' radius = 1, 17 | #' num.edges = c(4, 1000) 18 | #' ) 19 | #' perimeter.df = compute_polygon_perimeter(vertex.df) 20 | #' 21 | #' # The perimeters should be around 4*sqrt(2) and 2*pi 22 | #' print(perimeter.df) 23 | #' 24 | #' @export 25 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 26 | compute_polygon_perimeter = function(vertex.df) { 27 | check_vertex_df(vertex.df) 28 | n = nrow(vertex.df) 29 | 30 | if (!("group" %in% colnames(vertex.df))) { 31 | vertex.df$group = 1 32 | } 33 | vertex.df = vertex.df[order(vertex.df$group), ] 34 | group.count = as.numeric(table(vertex.df$group)) 35 | num.groups = length(unique(vertex.df$group)) 36 | x1 = vertex.df$x 37 | y1 = vertex.df$y 38 | # Picks out the index of the next vertex in each polygon, where the first vertex 39 | # is repeated after the last 40 | next.index = 1:n - rep(c(0, cumsum(group.count))[1:num.groups], group.count) 41 | next.index = c(next.index[-1], 1) 42 | next.index = next.index + rep(c(0, cumsum(group.count))[1:num.groups], group.count) 43 | x2 = vertex.df$x[next.index] 44 | y2 = vertex.df$y[next.index] 45 | edge.length = sqrt((x2 - x1)^2 + (y2 - y1)^2) 46 | 47 | # Compute sum of edge.length per group 48 | perimeter.df = stats::aggregate(edge.length, by = list(group = vertex.df$group), FUN = sum) 49 | colnames(perimeter.df) = c("group", "perimeter") 50 | 51 | return(perimeter.df) 52 | } 53 | -------------------------------------------------------------------------------- /R/generate_random_polygon.R: -------------------------------------------------------------------------------- 1 | #' Generate random polygon 2 | #' 3 | #' This function generates a random polygon. It uses the polar coordinate 4 | #' representation, where the angles are evenly spaced over the interval [0, 2pi] 5 | #' and the radiuses are sampled from a uniform distribution. 6 | #' 7 | #' @param center The "center" of the polygon, which determines its position. Should 8 | #' either be a 1 x 2 data frame/matrix or a numeric vector with 2 elements. 9 | #' @param min.radius,max.radius The minimum and maximum bounds used in the uniform 10 | #' distribution that samples the radius values 11 | #' @param num.vertices The number of vertices that the random polygon should contain 12 | #' @param seed The seed used when sampling the radius value 13 | #' 14 | #' @return A data frame that contains one row per vertex in the polygon, and the 15 | #' columns \code{x} and \code{y}. 16 | #' 17 | #' @examples 18 | #' # A random polygon with center (5, 10) and 10 vertices. The distance from 19 | #' # the center to the vertices is between 4 and 10 20 | #' vertex.df = generate_random_polygon( 21 | #' center = c(5, 10), 22 | #' min.radius = 4, 23 | #' max.radius = 10, 24 | #' num.vertices = 10, 25 | #' seed = 123 26 | #' ) 27 | #' 28 | #' library(ggplot2) 29 | #' # The center of the polygon is shown in red 30 | #' ggplot()+ 31 | #' geom_polygon(data = vertex.df, aes(x = x, y = y))+ 32 | #' geom_point(aes(x = 5, y = 10), color = "red")+ 33 | #' coord_fixed() 34 | #' 35 | #' @export 36 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 37 | generate_random_polygon = function(center = c(0, 0), min.radius = 0.5, max.radius = 1, num.vertices = 5, seed = NULL) { 38 | if (!is.null(seed)) { 39 | set.seed(seed) 40 | } 41 | center = as.numeric(center) 42 | theta = seq(0, 2*pi, length.out = num.vertices + 1)[-(num.vertices + 1)] 43 | radius = stats::runif(num.vertices, min.radius, max.radius) 44 | vertex.df = data.frame( 45 | x = center[1] + radius*cos(theta), 46 | y = center[2] + radius*sin(theta) 47 | ) 48 | return(vertex.df) 49 | } 50 | -------------------------------------------------------------------------------- /R/compute-polygon-area.R: -------------------------------------------------------------------------------- 1 | #' Compute polygon area 2 | #' 3 | #' This function takes in a data frame containing one or more polygons, 4 | #' and computes the (signed) area of each polygon. 5 | #' 6 | #' @inheritParams cut_polygons 7 | #' @param signed If true, the signed areas of the polygons are returned (positive for 8 | #' counter-clockwise oriented polygons, negative for clockwise oriented.) 9 | #' 10 | #' @return A data frame that contains two columns: 11 | #' \item{group}{The group number of the polygon} 12 | #' \item{area}{The area of the polygon} 13 | #' 14 | #' @examples 15 | #' # Example with multiple polygons 16 | #' vertex.df = compute_regular_polygons( 17 | #' center = data.frame(x = c(0, 10), y = c(0, 0)), 18 | #' radius = 1, 19 | #' num.edges = c(4, 1000) 20 | #' ) 21 | #' area.df = compute_polygon_area(vertex.df) 22 | #' 23 | #' # Note that the area of polygon 2 is close to pi! 24 | #' print(area.df) 25 | #' 26 | #' @export 27 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 28 | compute_polygon_area = function(vertex.df, signed = FALSE) { 29 | check_vertex_df(vertex.df) 30 | n = nrow(vertex.df) 31 | 32 | if (!("group" %in% colnames(vertex.df))) { 33 | vertex.df$group = 1 34 | } 35 | #vertex.df = vertex.df[order(vertex.df$group), ] 36 | group.count = as.numeric(table(vertex.df$group)) 37 | group.count = group.count[order(unique(vertex.df$group))] 38 | num.groups = length(unique(vertex.df$group)) 39 | x1 = vertex.df$x 40 | y1 = vertex.df$y 41 | # Picks out the index of the next vertex in each polygon, where the first vertex 42 | # is repeated after the last 43 | next.index = 1:n - rep(c(0, cumsum(group.count))[1:num.groups], group.count) 44 | next.index = c(next.index[-1], 1) 45 | next.index = next.index + rep(c(0, cumsum(group.count))[1:num.groups], group.count) 46 | x2 = vertex.df$x[next.index] 47 | y2 = vertex.df$y[next.index] 48 | cross.prod = 1/2*(x1*y2 - x2*y1) 49 | # Convert group to factor to preserve order 50 | vertex.df$group = factor(vertex.df$group, levels = unique(vertex.df$group)) 51 | # Compute sum of cross.prod per group 52 | area.df = stats::aggregate(cross.prod, by = list(group = vertex.df$group), FUN = sum) 53 | # Turn group back to numeric 54 | area.df$group = as.numeric(levels(area.df$group)) 55 | colnames(area.df) = c("group", "area") 56 | 57 | if (!signed) area.df$area = abs(area.df$area) 58 | return(area.df) 59 | } 60 | -------------------------------------------------------------------------------- /R/compute-polygon-centroid.R: -------------------------------------------------------------------------------- 1 | #' Compute polygon centroid 2 | #' 3 | #' This function takes in a data frame containing one or more polygons, 4 | #' and computes the centroid (geometric center) of each polygon. 5 | #' 6 | #' @inheritParams cut_polygons 7 | #' 8 | #' @return A data frame that contains three columns: 9 | #' \item{group}{The group number of the polygon} 10 | #' \item{x, y}{The coordinates of the centroid} 11 | #' 12 | #' 13 | #' @examples 14 | #' # Example with multiple polygons 15 | #' vertex.df = compute_regular_polygons( 16 | #' center = data.frame(x = c(0, 10), y = c(0, 0)), 17 | #' radius = 1, 18 | #' num.edges = c(4, 1000) 19 | #' ) 20 | #' centroid.df = compute_polygon_centroid(vertex.df) 21 | #' 22 | #' # Centroids should be (0, 0) and c(10, 0) 23 | #' print(centroid.df) 24 | #' 25 | #' @export 26 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 27 | compute_polygon_centroid = function(vertex.df) { 28 | check_vertex_df(vertex.df) 29 | n = nrow(vertex.df) 30 | 31 | if (!("group" %in% colnames(vertex.df))) { 32 | vertex.df$group = 1 33 | } 34 | 35 | area.df = compute_polygon_area(vertex.df, signed = TRUE) 36 | group.count = as.numeric(table(vertex.df$group)) 37 | group.count = group.count[order(unique(vertex.df$group))] 38 | num.groups = length(unique(vertex.df$group)) 39 | area.df = area.df[rep(1:num.groups, group.count), ] 40 | x1 = vertex.df$x 41 | y1 = vertex.df$y 42 | # Picks out the index of the next vertex in each polygon, where the first vertex 43 | # is repeated after the last 44 | next.index = 1:n - rep(c(0, cumsum(group.count))[1:num.groups], group.count) 45 | next.index = c(next.index[-1], 1) 46 | next.index = next.index + rep(c(0, cumsum(group.count))[1:num.groups], group.count) 47 | x2 = vertex.df$x[next.index] 48 | y2 = vertex.df$y[next.index] 49 | # Convert group to factor to preserve order 50 | vertex.df$group = factor(vertex.df$group, levels = unique(vertex.df$group)) 51 | 52 | x.parts = 1/(6*area.df$area)*(x1 + x2)*(x1*y2 - x2*y1) 53 | x.centroid = stats::aggregate(x.parts, by = list(group = vertex.df$group), FUN = sum) 54 | 55 | y.parts = 1/(6*area.df$area)*(y1 + y2)*(x1*y2 - x2*y1) 56 | y.centroid = stats::aggregate(y.parts, by = list(group = vertex.df$group), FUN = sum) 57 | 58 | centroid.df = x.centroid 59 | centroid.df$y = y.centroid$x 60 | centroid.df$group = as.numeric(levels(centroid.df$group)) 61 | return(centroid.df) 62 | } 63 | -------------------------------------------------------------------------------- /R/rounded-regular-polygon.R: -------------------------------------------------------------------------------- 1 | #' Generate regular polygon with rounded corners 2 | #' 3 | #' This function computes the vertices of a regular polygon with rounded corners. 4 | #' 5 | #' @param num.edges The number of edges that the regular polygon contains. 6 | #' @param center The center/centroid of the shape. Can either be a 1 x 2 data 7 | #' frame/matrix or a numeric vector length 2. 8 | #' @param radius The radius of the circumscribed circle of the regular polygon. 9 | #' @param rotation The amount of rotation applied to the shape, in radians. 10 | #' @param corner.radius The radius of the circular arcs. 11 | #' @param vertices.per.corner The number of vertices that each circular arc consists of. 12 | #' 13 | #' @note The result may contain less than \code{num.edges*vertices.per.corner} vertices. 14 | #' In certain situations, consecutive vertices will end up having the same coordinates. 15 | #' The duplicates are then removed. 16 | #' 17 | #' @return A data frame containing the columns \code{x} and \code{y}, where each 18 | #' row is a vertex in the rounded polygon. 19 | #' 20 | #' @examples 21 | #' # Create a rounded hexagon 22 | #' vertex.df = rounded_regular_polygon( 23 | #' num.edges = 6, 24 | #' center = c(5, 6), 25 | #' radius = 2, 26 | #' rotation = pi/4, 27 | #' corner.radius = 0.5 28 | #' ) 29 | #' 30 | #' # How the rounded hexagon looks: 31 | #' library(ggplot2) 32 | #' ggplot()+ 33 | #' geom_polygon(data = vertex.df, aes(x = x, y = y))+ 34 | #' coord_fixed() 35 | #' 36 | #' @export 37 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 38 | rounded_regular_polygon = function(num.edges = 3, center = c(0, 0), radius = 1, rotation = 0, corner.radius = 0.25, vertices.per.corner = 50) { 39 | interior.angle = (num.edges - 2) / num.edges * pi 40 | corner.centers = compute_regular_polygons(center, radius, -pi/2, num.edges, include.info = FALSE)[, c("x", "y")] 41 | corner.centers = deform_polygon(corner.centers, -corner.radius/sin(interior.angle / 2)) 42 | corner.centers.total = corner.centers[rep(1:num.edges, each = vertices.per.corner), ] 43 | 44 | theta.arc = seq(-pi/num.edges, pi/num.edges, length.out = vertices.per.corner) 45 | theta.offset = seq(0, 2*pi, length.out = num.edges + 1)[-(num.edges + 1)] 46 | theta.arc.total = rep(theta.arc, num.edges) + rep(theta.offset, each = vertices.per.corner) 47 | 48 | rounded.parts = corner.radius*cbind(cos(theta.arc.total), sin(theta.arc.total)) 49 | rounded.polygon = corner.centers.total + rounded.parts 50 | 51 | # Offset by pi/2 to move first vertex to top when rotation = 0 52 | rotation = pi/2 + rotation 53 | rotated.polygon = rotate_polygon(rounded.polygon, rotation, center.of.rotation = center)[, c("x", "y")] 54 | return(rotated.polygon) 55 | } 56 | -------------------------------------------------------------------------------- /R/rounded-rectangle.R: -------------------------------------------------------------------------------- 1 | #' Generate rectangle with rounded corners 2 | #' 3 | #' This function computes the vertices of a rectangle with rounded corners. 4 | #' 5 | #' @param center The center/centroid of the shape. Can either be a 1 x 2 data 6 | #' frame/matrix or a numeric vector length 2. 7 | #' @param width,height The width and height of the rectangle before the corners 8 | #' are rounded. 9 | #' @param rotation The amount of rotation applied to the shape, in radians. 10 | #' @param corner.radius The radius of the circular arcs. Should be less than min(width, height) / 2. 11 | #' @param vertices.per.corner The number of vertices that each circular arc consists of. 12 | #' 13 | #' @note The result may contain less than \code{4*vertices.per.corner} vertices. 14 | #' In certain situations, consecutive vertices will end up having the same coordinates. 15 | #' The duplicates are then removed. 16 | #' 17 | #' @return A data frame containing the columns \code{x} and \code{y}, where each 18 | #' row is a vertex in the rounded rectangle. 19 | #' 20 | #' @examples 21 | #' # Create a rounded rectangle 22 | #' vertex.df = rounded_rectangle( 23 | #' center = c(2, 5), 24 | #' width = 3, 25 | #' height = 6, 26 | #' rotation = pi/4, 27 | #' corner.radius = 1 28 | #' ) 29 | #' 30 | #' # How the rounded rectangle looks: 31 | #' library(ggplot2) 32 | #' ggplot()+ 33 | #' geom_polygon(data = vertex.df, aes(x = x, y = y))+ 34 | #' coord_fixed() 35 | #' 36 | #' @export 37 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 38 | rounded_rectangle = function(center = c(0, 0), width = 1, height = 1, rotation = 0, corner.radius = NULL, vertices.per.corner = 50) { 39 | if (is.null(corner.radius)) { 40 | corner.radius = min(width, height) / 4 41 | } else if (corner.radius > min(width, height) / 2) { 42 | warning("corner.radius should be less than min(width, height) / 2.") 43 | } 44 | center = as.numeric(center) 45 | inner.width = width - 2*corner.radius 46 | inner.height = height - 2*corner.radius 47 | corner.offsets = data.frame(x = inner.width*c(1, -1, -1, 1), y = inner.height*c(1, 1, -1, -1)) / 2 48 | corner.centers = matrix(center, nrow = 4, ncol = 2, byrow = TRUE) + corner.offsets 49 | 50 | corner.centers.ext = corner.centers[rep(1:4, each = vertices.per.corner), ] 51 | theta = rep(seq(0, pi/2, length.out = vertices.per.corner), 4) 52 | theta = theta + rep(seq(0, 2*pi, length.out = 5)[-5], each = vertices.per.corner) 53 | circle = corner.radius*cbind(cos(theta), sin(theta)) 54 | rounded.rectangle = corner.centers.ext + circle 55 | 56 | rotated.rectangle = rotate_polygon(rounded.rectangle, rotation, center)[, c("x", "y")] 57 | rotated.rectangle = remove_duplicate_vertices(rotated.rectangle) 58 | return(rotated.rectangle) 59 | } 60 | -------------------------------------------------------------------------------- /src/RcppExports.cpp: -------------------------------------------------------------------------------- 1 | // Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | // Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | #include 5 | 6 | using namespace Rcpp; 7 | 8 | #ifdef RCPP_USE_GLOBAL_ROSTREAM 9 | Rcpp::Rostream& Rcpp::Rcout = Rcpp::Rcpp_cout_get(); 10 | Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); 11 | #endif 12 | 13 | // cutEveryPolygonRcpp 14 | Rcpp::DataFrame cutEveryPolygonRcpp(Rcpp::NumericVector x, Rcpp::NumericVector y, Rcpp::NumericVector group, int numberOfIterations, bool useCentroid); 15 | RcppExport SEXP _artKIT_cutEveryPolygonRcpp(SEXP xSEXP, SEXP ySEXP, SEXP groupSEXP, SEXP numberOfIterationsSEXP, SEXP useCentroidSEXP) { 16 | BEGIN_RCPP 17 | Rcpp::RObject rcpp_result_gen; 18 | Rcpp::RNGScope rcpp_rngScope_gen; 19 | Rcpp::traits::input_parameter< Rcpp::NumericVector >::type x(xSEXP); 20 | Rcpp::traits::input_parameter< Rcpp::NumericVector >::type y(ySEXP); 21 | Rcpp::traits::input_parameter< Rcpp::NumericVector >::type group(groupSEXP); 22 | Rcpp::traits::input_parameter< int >::type numberOfIterations(numberOfIterationsSEXP); 23 | Rcpp::traits::input_parameter< bool >::type useCentroid(useCentroidSEXP); 24 | rcpp_result_gen = Rcpp::wrap(cutEveryPolygonRcpp(x, y, group, numberOfIterations, useCentroid)); 25 | return rcpp_result_gen; 26 | END_RCPP 27 | } 28 | // cutEveryPolygonReturnAllRcpp 29 | Rcpp::DataFrame cutEveryPolygonReturnAllRcpp(Rcpp::NumericVector x, Rcpp::NumericVector y, Rcpp::NumericVector group, int numberOfIterations, bool useCentroid); 30 | RcppExport SEXP _artKIT_cutEveryPolygonReturnAllRcpp(SEXP xSEXP, SEXP ySEXP, SEXP groupSEXP, SEXP numberOfIterationsSEXP, SEXP useCentroidSEXP) { 31 | BEGIN_RCPP 32 | Rcpp::RObject rcpp_result_gen; 33 | Rcpp::RNGScope rcpp_rngScope_gen; 34 | Rcpp::traits::input_parameter< Rcpp::NumericVector >::type x(xSEXP); 35 | Rcpp::traits::input_parameter< Rcpp::NumericVector >::type y(ySEXP); 36 | Rcpp::traits::input_parameter< Rcpp::NumericVector >::type group(groupSEXP); 37 | Rcpp::traits::input_parameter< int >::type numberOfIterations(numberOfIterationsSEXP); 38 | Rcpp::traits::input_parameter< bool >::type useCentroid(useCentroidSEXP); 39 | rcpp_result_gen = Rcpp::wrap(cutEveryPolygonReturnAllRcpp(x, y, group, numberOfIterations, useCentroid)); 40 | return rcpp_result_gen; 41 | END_RCPP 42 | } 43 | 44 | static const R_CallMethodDef CallEntries[] = { 45 | {"_artKIT_cutEveryPolygonRcpp", (DL_FUNC) &_artKIT_cutEveryPolygonRcpp, 5}, 46 | {"_artKIT_cutEveryPolygonReturnAllRcpp", (DL_FUNC) &_artKIT_cutEveryPolygonReturnAllRcpp, 5}, 47 | {NULL, NULL, 0} 48 | }; 49 | 50 | RcppExport void R_init_artKIT(DllInfo *dll) { 51 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 52 | R_useDynamicSymbols(dll, FALSE); 53 | } 54 | -------------------------------------------------------------------------------- /man/compute_regular_polygons.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/compute-regular-polygons.R 3 | \name{compute_regular_polygons} 4 | \alias{compute_regular_polygons} 5 | \title{Compute regular polygons} 6 | \usage{ 7 | compute_regular_polygons( 8 | center = c(0, 0), 9 | radius = 1, 10 | rotation = 0, 11 | num.edges = 50, 12 | include.info = TRUE, 13 | ... 14 | ) 15 | } 16 | \arguments{ 17 | \item{center}{Specifies the center(s) of the polygon(s). Can either be a 18 | numeric vector of length 2 (every polygon has the same center), or a 19 | data frame/matrix with 1 row per polygon. If the data frame/matrix has 20 | columns named \code{x} and \code{y}, these are used as center coordinates. 21 | Otherwise, the first two columns are used.} 22 | 23 | \item{radius}{The radius of the circumscribed circle of the polygon. Can either 24 | be a single number or a vector containing one radius per polygon.} 25 | 26 | \item{rotation}{The amount of rotation applied to the polygon(s), in radians. 27 | If \code{rotation = 0}, then the first vertex is at the "top" of the polygon. 28 | Can either be a single number or a vector containing a different rotation per polygon.} 29 | 30 | \item{num.edges}{The number of edges that the polygon(s) contain. Can either 31 | be a single integer or a vector containing a different number of edges per polygon.} 32 | 33 | \item{include.info}{If true, the returned data frame will contain the values for 34 | \code{radius}, \code{rotation} and \code{num.edges} as columns.} 35 | 36 | \item{...}{Custom properties that can be used to store additional information about 37 | the polygon(s), such as color and opacity. Can either be a single value or a 38 | vector containing a different value per polygon.} 39 | } 40 | \value{ 41 | A data frame where each row corresponds to the vertex of a polygon. 42 | The following columns are included: 43 | \item{x, y}{The coordinates of the vertex} 44 | \item{group}{The group number of the polygon} 45 | \item{radius, rotation, num.edges}{ The parameters that are specified in the function call. 46 | Only included if \code{include.info = TRUE}} 47 | \item{...}{Custom columns that are specified in the function call.} 48 | } 49 | \description{ 50 | This function computes one or more regular polygons. 51 | } 52 | \examples{ 53 | set.seed(123) 54 | # This generates 10 polygons with different centers, an individual number of edges 55 | # and a custom property "color" 56 | vertex.df = compute_regular_polygons( 57 | center = data.frame(x = 1:10, y = 1:10), 58 | radius = 0.5, 59 | num.edges = 3:12, 60 | color = sample(c("red", "blue", "green"), 10, replace = TRUE) 61 | ) 62 | 63 | library(ggplot2) 64 | ggplot()+ 65 | geom_polygon(data = vertex.df, aes(x = x, y = y, group = group, fill = color))+ 66 | scale_fill_identity()+ 67 | coord_fixed() 68 | 69 | 70 | } 71 | \author{ 72 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 73 | } 74 | -------------------------------------------------------------------------------- /man/cut_polygons.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/cut-polygons.R 3 | \name{cut_polygons} 4 | \alias{cut_polygons} 5 | \title{Repeated cutting of polygons} 6 | \usage{ 7 | cut_polygons( 8 | vertex.df, 9 | number.of.iterations, 10 | use.centroid = TRUE, 11 | seed = NULL, 12 | return.all = FALSE 13 | ) 14 | } 15 | \arguments{ 16 | \item{vertex.df}{A data frame where each row corresponds to the vertex of a polygon. 17 | It must contain the columns \code{x}, \code{y}, and optionally \code{group} (must be integer). 18 | \code{x} and \code{y} specify the coordinates of the vertex, 19 | and \code{group} is used to indicate which polygon the vertex belongs to. 20 | If \code{vertex.df} contains only a single polygon, \code{group} can be omitted.} 21 | 22 | \item{number.of.iterations}{The number of times that the polygons are cut in 23 | half. Either a single value, or a numeric vector with one value per polygon in \code{vertex.df}.} 24 | 25 | \item{use.centroid}{If \code{TRUE}, the randomly generated line goes through the centroid 26 | of the polygon. Otherwise, the line goes through a point sampled uniformly 27 | from the interior of the polygon.} 28 | 29 | \item{seed}{The seed used when generating the directions of the cut lines. 30 | When no value is specified, the seed is not set.} 31 | 32 | \item{return.all}{If \code{TRUE}, the function will return a data frame 33 | that contains every iteration (i.e. the state after iteration 1,...,\code{number.of.iterations}).} 34 | } 35 | \value{ 36 | A data frame where each row corresponds to the vertex of a polygon, 37 | with the following columns: 38 | \item{x, y}{The coordinates of the vertex} 39 | \item{group}{The index of the polygon that the vertex belongs to} 40 | \item{iteration}{The iteration that the polygon belongs to (only present when \code{return.all = TRUE})} 41 | } 42 | \description{ 43 | This function takes in an arbitrary number of polygons and cuts them a given 44 | number of times. For each iteration, every polygon is cut by a randomly generated 45 | line, which divides it into several parts. 46 | } 47 | \details{ 48 | The directions of the lines are sampled from a uniform distribution. 49 | } 50 | \note{ 51 | \code{number.of.iterations} should be chosen with care, as each 52 | iteration at least doubles the number of polygons, leading to exponential growth. 53 | } 54 | \examples{ 55 | # Example with single initial polygon (square) 56 | vertex.df = data.frame( 57 | x = c(-1, 1, 1, -1), 58 | y = c(1, 1, -1, -1) 59 | ) 60 | cut.df = cut_polygons(vertex.df, number.of.iterations = 10, seed = 123) 61 | 62 | library(ggplot2) 63 | ggplot()+ 64 | geom_polygon(data = cut.df, 65 | aes(x = x, y = y, group = group), 66 | fill = NA, color = "black")+ 67 | coord_fixed() 68 | 69 | # Example with multiple initial polygons (square and triangle) 70 | vertex.df = data.frame( 71 | x = c(-1, 1, 1, -1, 1, -1, 0), 72 | y = c(1, 1, -1, -1, 2, 2, 2+sqrt(3)), 73 | group = c(rep(1, 4), rep(2, 3)) 74 | ) 75 | 76 | # Cut square and triangle 10 and 9 times, respectively 77 | cut.df = cut_polygons(vertex.df, number.of.iterations = c(10, 9), seed = 123) 78 | 79 | ggplot()+ 80 | geom_polygon(data = cut.df, 81 | aes(x = x, y = y, group = group), 82 | fill = NA, color = "black")+ 83 | coord_fixed() 84 | 85 | } 86 | \author{ 87 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 88 | } 89 | -------------------------------------------------------------------------------- /man/rotate_polygon.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rotate-polygon.R 3 | \name{rotate_polygon} 4 | \alias{rotate_polygon} 5 | \title{Rotate polygons} 6 | \usage{ 7 | rotate_polygon(vertex.df, rotation, center.of.rotation = "centroid") 8 | } 9 | \arguments{ 10 | \item{vertex.df}{A data frame where each row corresponds to the vertex of a polygon. 11 | It must contain the columns \code{x}, \code{y}, and optionally \code{group} (must be integer). 12 | \code{x} and \code{y} specify the coordinates of the vertex, 13 | and \code{group} is used to indicate which polygon the vertex belongs to. 14 | If \code{vertex.df} contains only a single polygon, \code{group} can be omitted.} 15 | 16 | \item{rotation}{The amount each polygon is to be rotated in radians. Either a single 17 | number or a vector containing one number per polygon in \code{vertex.df}.} 18 | 19 | \item{center.of.rotation}{The center of rotation, i.e. the point which the polygons 20 | will be rotated around. Can be one of the following: 21 | \itemize{ 22 | \item{"centroid" (default), where each polygon is rotated around its centroid} 23 | \item{A 1 x 2 matrix data frame/matrix that specifices the x- and y-coordinate 24 | of the center of rotation used for every polygon (a numeric vector of length 2 25 | is also accepted, i.e. c(x, y))} 26 | \item{A n x 2 data frame/matrix, where row number i contains the x- and y-coordinates of 27 | the center of rotation for polygon number i in \code{vertex.df}, and n is the number of polygons.} 28 | }} 29 | } 30 | \value{ 31 | The returned data frame is equal to \code{vertex.df} everywhere, except 32 | the \code{x}- and \code{y}-columns that are changed due to the rotation. 33 | } 34 | \description{ 35 | Takes in one or more polygons and rotates each one a specified amount. 36 | } 37 | \examples{ 38 | polygon.center = data.frame(x = c(0, 2, 4), y = c(0, 2, 4)) 39 | original.df = compute_regular_polygons( 40 | center = polygon.center, 41 | radius = c(1, 1, 1.5), 42 | num.edges = 3:5 43 | ) 44 | 45 | # Rotating every polygon the same amount, using the centroids as centers of rotation 46 | centroid.df = rotate_polygon(original.df, rotation = pi/8, center.of.rotation = "centroid") 47 | # Rotating each polygon a different amount, around a different center of rotation 48 | center.of.rotation = data.frame(x = c(2, 0, 4), y = c(0, 2, 0)) 49 | manual.df = rotate_polygon( 50 | original.df, 51 | rotation = c(pi/4, pi, -pi/2), 52 | center.of.rotation = center.of.rotation 53 | ) 54 | 55 | # The plot below shows both the original and rotated polygons. 56 | # The black lines connect the center of rotation used in manual.df to the centroids 57 | # of the original polygons. 58 | library(ggplot2) 59 | ggplot()+ 60 | geom_polygon(data = original.df, aes(x = x, y = y, group = group, color = "1. Original"), 61 | fill = NA)+ 62 | geom_polygon(data = centroid.df, aes(x = x, y = y, group = group, color = "2. Centroid"), 63 | fill = NA)+ 64 | geom_polygon(data = manual.df, aes(x = x, y = y, group = group, color = "3. Manual"), 65 | fill = NA)+ 66 | geom_segment(aes(x = polygon.center$x, xend = center.of.rotation$x, 67 | y = polygon.center$y, yend = center.of.rotation$y))+ 68 | scale_color_manual("", values = c("red", "green", "blue"))+ 69 | coord_fixed() 70 | 71 | 72 | } 73 | \author{ 74 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 75 | } 76 | -------------------------------------------------------------------------------- /man/transition_between_polygons.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/transition-between-polygons.R 3 | \name{transition_between_polygons} 4 | \alias{transition_between_polygons} 5 | \title{Transition between polygons} 6 | \usage{ 7 | transition_between_polygons( 8 | start.polygon, 9 | end.polygon, 10 | time, 11 | vertex.order = "reorder" 12 | ) 13 | } 14 | \arguments{ 15 | \item{start.polygon, end.polygon}{Dataframes/matrices containing the vertices of the 16 | polygons to interpolate between. The coordinates of the vertices must be stored in columns named "x" and "y", 17 | and the polygons must contain the same number of vertices.} 18 | 19 | \item{time}{The times at which we are interested in interpolating the polygons. \code{time = 0} gives 20 | \code{start.polygon}, while \code{time = 1} gives \code{end.polygon}. Can be either a single number 21 | or a numeric vector. If time contains values outside [0, 1], a warning will be given.} 22 | 23 | \item{vertex.order}{Determines whether and how the vertices in \code{start.polygon} and \code{end.polygon} 24 | are reordered before the interpolation is computed. If \code{vertex.order = "preserve"}, no 25 | reordering is applied, and the polygons are used as-is. If \code{vertex.order = "reorder"}, 26 | the function first ensures that the polygons have the same orientation (i.e. clockwise/counter-clockwise). 27 | Then, it attempts to shift the indices of the vertices so that the corresponding vertices on 28 | \code{start.polygon} and \code{end.polygon} are "aligned".} 29 | } 30 | \value{ 31 | A data frame that contains one row per vertex. If \code{start.polygon} 32 | and \code{end.polygon} contain n vertices, and \code{time} contains m values, then 33 | the returned data frame will contain n*m rows. following columns: 34 | \item{x, y}{The coordinates of the vertex} 35 | \item{group}{Which polygon the vertex belongs to (1 for the first value in \code{time}, 2 for the second and so on)} 36 | \item{time}{The time value of the associated polygon} 37 | } 38 | \description{ 39 | Function that interpolates/transitions between two polygons. This is done by 40 | computing \code{(1 - time)*start.polygon + time*end.polygon}. 41 | } 42 | \note{ 43 | It is recommended to ensure that the start and end polygons have the correct 44 | orientation and numbering of vertices before computing the transition, and then using 45 | \code{vertex.order = "preserve"}. 46 | } 47 | \examples{ 48 | # Example: Transition from hexagon to square 49 | # Create hexagon 50 | hexagon.df = compute_regular_polygons( 51 | center = c(0, 0), 52 | radius = 1, 53 | rotation = 0, 54 | num.edges = 6 55 | ) 56 | # Round corners slightly 57 | hexagon.df = round_polygon_corners(hexagon.df, corner.radius.scale = 0.3) 58 | 59 | # Create square 60 | square.df = compute_regular_polygons( 61 | center = c(20, -20), 62 | radius = 2, 63 | rotation = 0, 64 | num.edges = 4 65 | ) 66 | # Round corners slightly 67 | square.df = round_polygon_corners(square.df, corner.radius.scale = 0.3) 68 | 69 | # Resample polygons with many vertices, so that the transition becomes smooth 70 | num.vertices = 1000 71 | resample.time = seq(0, 1, length.out = num.vertices + 1)[-(num.vertices + 1)] 72 | hexagon.resample = interpolate_polygon(hexagon.df)(resample.time) 73 | square.resample = interpolate_polygon(square.df)(resample.time) 74 | 75 | # Show transition over 10 steps 76 | num.transition = 10 77 | transition.time = seq(0, 1, length.out = num.transition) 78 | # Use vertex.order = "preserve" (both polygons are CCW, and have the top vertex 79 | # as the first in hexagon.df and square.df) 80 | transition.df = transition_between_polygons( 81 | hexagon.resample, 82 | square.resample, 83 | transition.time, 84 | "preserve") 85 | 86 | # Show the result: 87 | library(ggplot2) 88 | ggplot()+ 89 | geom_polygon(data = transition.df, aes(x = x, y = y, group = group), fill = NA, color = "black")+ 90 | coord_fixed() 91 | 92 | } 93 | \author{ 94 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 95 | } 96 | -------------------------------------------------------------------------------- /R/utilities.R: -------------------------------------------------------------------------------- 1 | 2 | generate_grid_centers = function(x.lower, x.upper, y.lower, y.upper, resolution.x, resolution.y, params, separate = FALSE) { 3 | if (!missing(params)) { 4 | # Unpack parameters to function scope 5 | list2env(params, environment()) 6 | } 7 | h.x = (x.upper-x.lower)/resolution.x 8 | h.y = (y.upper-y.lower)/resolution.y 9 | x.grid = seq(x.lower+h.x/2, x.upper-h.x/2, length.out = resolution.x) 10 | y.grid = seq(y.lower+h.y/2, y.upper-h.y/2, length.out = resolution.y) 11 | if (separate) { 12 | return(list(x = x.grid, y = y.grid)) 13 | } else { 14 | s = expand.grid(x.grid, y.grid) 15 | colnames(s) = c("x", "y") 16 | return(s) 17 | } 18 | } 19 | 20 | gaussian_kernel = function(x, sd = 1) { 21 | exp(-(x/sd)^2) 22 | } 23 | 24 | contains_columns = function(input.df, column.names) { 25 | all(sapply(column.names, function(name) name %in% colnames(input.df))) 26 | } 27 | 28 | check_vertex_df = function(vertex.df, df.name = "vertex.df") { 29 | if (!contains_columns(vertex.df, c("x", "y"))) { 30 | stop(df.name, " must contain columns \"x\" and \"y\".") 31 | } 32 | } 33 | 34 | is_polygon_ccw = function(vertex.df) { 35 | check_vertex_df(vertex.df) 36 | n = nrow(vertex.df) 37 | 38 | x.min = min(vertex.df$x) 39 | pivot.ind = which(vertex.df$x == x.min) 40 | if (length(pivot.ind) > 1) { 41 | # If there are ties for vertices with smallest x-coordinate, use the one 42 | # with the largest y-coordinate 43 | pivot.ind = pivot.ind[which.max(vertex.df$y[pivot.ind])] 44 | } 45 | A = vertex.df[ifelse(pivot.ind - 1 == 0, n, pivot.ind - 1), ] 46 | B = vertex.df[pivot.ind, ] 47 | C = vertex.df[ifelse(pivot.ind + 1 == n + 1, 1, pivot.ind + 1), ] 48 | 49 | det = (B$x - A$x)*(C$y - A$y) - (C$x - A$x)*(B$y - A$y) 50 | return(det > 0) 51 | } 52 | 53 | # Inefficient, should be improved 54 | closest_pair_of_points = function(points.1, points.2) { 55 | n.1 = nrow(points.1) 56 | n.2 = nrow(points.2) 57 | 58 | repeat.1 = points.1[rep(1:n.1, each = n.2), ] 59 | repeat.2 = points.2[rep(1:n.2, n.1), ] 60 | 61 | diff = repeat.1 - repeat.2 62 | dist2 = rep(0, nrow(diff)) 63 | for (i in 1:ncol(diff)) { 64 | dist2 = dist2 + diff[, i]^2 65 | } 66 | closest.index = unname(which.min(dist2)) 67 | index.1 = ceiling(closest.index / n.2) 68 | index.2 = (closest.index - 1) %% n.2 + 1 69 | return(c(index.1, index.2)) 70 | } 71 | 72 | # Inefficient, should be improved 73 | multiple_closest_values = function(values.1, values.2, num.closest = 1) { 74 | n.1 = length(values.1) 75 | n.2 = length(values.2) 76 | if (num.closest > n.1 || num.closest > n.2) { 77 | stop("num.closest cannot be greater than the length either values.1 or values.2.") 78 | } 79 | 80 | matrix.1 = matrix(values.1, nrow = n.1, ncol = n.2, byrow = FALSE) 81 | matrix.2 = matrix(values.2, nrow = n.1, ncol = n.2, byrow = TRUE) 82 | 83 | diff = abs(matrix.1 - matrix.2) 84 | col.names = 1:n.2 85 | row.names = 1:n.1 86 | index.matrix = data.frame(index.1 = rep(NA, num.closest), index.2 = rep(NA, num.closest)) 87 | 88 | for (i in 1:num.closest) { 89 | closest.indices = arrayInd(which.min(diff), dim(diff)) 90 | index.matrix[i, 1] = row.names[closest.indices[1]] 91 | index.matrix[i, 2] = col.names[closest.indices[2]] 92 | if (nrow(diff) == 2 || ncol(diff) == 2) { 93 | diff = matrix(diff[-closest.indices[1], -closest.indices[2]], 94 | nrow = nrow(diff) - 1, ncol = ncol(diff) - 1, byrow = nrow(diff) == 2) 95 | } else { 96 | diff = diff[-closest.indices[1], -closest.indices[2]] 97 | } 98 | 99 | row.names = row.names[-closest.indices[1]] 100 | col.names = col.names[-closest.indices[2]] 101 | } 102 | return(index.matrix) 103 | } 104 | 105 | remove_duplicate_vertices = function(vertex.df, min.distance = 1e-14) { 106 | vertex.distance2 = rowSums((vertex.df-vertex.df[c(2:nrow(vertex.df), 1), ])^2) 107 | vertex.df = vertex.df[vertex.distance2 > min.distance^2, ] 108 | return(vertex.df) 109 | } 110 | -------------------------------------------------------------------------------- /man/image_from_function.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/image-from-function.R 3 | \name{image_from_function} 4 | \alias{image_from_function} 5 | \title{Create image based on color function} 6 | \usage{ 7 | image_from_function( 8 | filename, 9 | color.function, 10 | bounds = c(-1, 1, -1, 1), 11 | width, 12 | height, 13 | num.portions = 1 14 | ) 15 | } 16 | \arguments{ 17 | \item{filename}{The filename of the output PNG file. It does not need to end 18 | in ".png".} 19 | 20 | \item{color.function}{A function that takes in an n x 2 data frame of locations 21 | inside the rectangle specified in \code{bounds}, and returns one color per location. 22 | The data frame has columns named \code{x} and \code{y}. 23 | The function can return the colors in one of the following formats: 24 | \itemize{ 25 | \item{A vector of n hex codes, e.g. "#FF0000" or "#123456AB" (where "AB" in the latter is alpha / transparency)} 26 | \item{A vector of n greyscale values between 0 and 1, where 0 is black and 1 is white} 27 | \item{An n x 2 matrix of values between 0 and 1, where the first column is the grayscale value and the second is alpha} 28 | \item{An n x 3 matrix of values between 0 and 1, where the columns correspond to R, G and B} 29 | \item{An n x 4 matrix of values between 0 and 1, where the columns correspond to R, G, B and alpha} 30 | }} 31 | 32 | \item{bounds}{A four-element vector that specifies the rectangular area used 33 | when evaluating \code{color.function}. The first two elements are the lower and 34 | upper bounds for the x-coordinate, while the last two are the lower and upper 35 | bounds for the y-coordinate.} 36 | 37 | \item{width, height}{The number of columns and rows in the output image, so that 38 | the total number of pixels is \code{width*height}.} 39 | 40 | \item{num.portions}{When this parameter is greater than 1, the output image 41 | will be divided into \code{num.portions} vertical slices of approximately 42 | the same height. This is useful for reducing memory use when \code{width} and 43 | \code{height} are large. The slices can be merged into the original image with 44 | image editing software.} 45 | } 46 | \description{ 47 | This function takes in a function that maps every location over some rectangular 48 | area to a color, and saves a PNG image based on the output of the function. 49 | } 50 | \note{ 51 | The aspect ratio of the output image (\code{width / height}) should be 52 | the same as the aspect ratio of \code{bounds}. If not, a warning is issued. 53 | } 54 | \examples{ 55 | \dontrun{ 56 | # Example with hex values 57 | color.function.hex = function(locations) { 58 | colors = c("#df361f", "#21747a", "#edd324") 59 | return(sample(colors, size = nrow(locations), replace = TRUE)) 60 | } 61 | 62 | image_from_function("hex.png", color.function.hex, 63 | width = 1000, height = 1000) 64 | 65 | # Example with grayscale 66 | color.function.gray = function(locations) { 67 | gray = minmax_scaling(locations$x^2 + locations$y^2) 68 | return(gray) 69 | } 70 | 71 | image_from_function("grayscale.png", color.function.gray, 72 | width = 1000, height = 1000) 73 | 74 | 75 | # Example with RGB matrix 76 | color.function.rgb = function(locations) { 77 | red = minmax_scaling(locations$x^2) 78 | green = minmax_scaling(sin(pi*locations$y), 0, 0.5) 79 | blue = minmax_scaling(locations$x*locations$y) 80 | return(cbind(red, green, blue)) 81 | } 82 | 83 | image_from_function("rgb.png", color.function.rgb, bounds = c(-2, 2, -1, 1), 84 | width = 2000, height = 1000) 85 | 86 | # Example with RGBA matrix 87 | color.function.rgba = function(locations) { 88 | red = minmax_scaling(sin(pi*locations$x)) 89 | green = minmax_scaling(sin(2*pi*locations$x + 0.25)) 90 | blue = minmax_scaling(sin(4*pi*locations$x + 0.5)) 91 | alpha = minmax_scaling(sin(6*pi*locations$x + 0.75)) 92 | return(cbind(red, green, blue, alpha)) 93 | } 94 | 95 | image_from_function("rgba.png", color.function.rgba, 96 | width = 1000, height = 1000) 97 | } 98 | } 99 | \author{ 100 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 101 | } 102 | -------------------------------------------------------------------------------- /man/round_polygon_corners.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/round-polygon-corners.R 3 | \name{round_polygon_corners} 4 | \alias{round_polygon_corners} 5 | \title{Rounds the corners of a polygon} 6 | \usage{ 7 | round_polygon_corners( 8 | vertex.df, 9 | corner.radius = "constant", 10 | corner.radius.scale = 1, 11 | max.vertices.per.corner = 50 12 | ) 13 | } 14 | \arguments{ 15 | \item{vertex.df}{A data frame where each row corresponds to a vertex in the polygon. 16 | It must contain the columns \code{x} and \code{y} where \code{x} and \code{y} 17 | specify the coordinates of the vertex,} 18 | 19 | \item{corner.radius}{Determines the corner radius of each circular art. Can be one 20 | of the following: 21 | \itemize{ 22 | \item{A single value, which will be used for every circular arc/corner} 23 | \item{A numeric vector of length nrow(vertex.df), where each value specifies 24 | the corner radius used for the corresponding vertex} 25 | \item{"constant", where every corner uses the same, optimal radius. This is the 26 | largest possible radius while keeping things "nice and smooth"} 27 | \item{"varying", which computes the largest possible corner radius for each vertex} 28 | }} 29 | 30 | \item{corner.radius.scale}{A number that each corner radius is multiplied by. 31 | Useful when \code{corner.radius} is "constant" or "varying", and you wish to 32 | reduce the size of the corner radius values.} 33 | 34 | \item{max.vertices.per.corner}{Controls the number of vertices used in the circular arcs 35 | that replace the corners. This depends on how sharp the corner is: the sharper the 36 | corner, the more vertices are needed to create a smooth arc.} 37 | } 38 | \value{ 39 | A data frame that contains the x- and y-coordinates of the rounded polygon. 40 | } 41 | \description{ 42 | Takes in a single polygon and rounds it by replacing the corner vertices with circular arcs. 43 | } 44 | \note{ 45 | If the corner radius values are specified manually, the results will not necessarily 46 | look good, and it may take some trial and error. The option "varying" is experimental, 47 | and can be bit aggressive. See the example for a comparison between the different 48 | options. 49 | } 50 | \examples{ 51 | # Generate a random polygon based on polar coordinates 52 | set.seed(321) 53 | n = 20 # Number of vertices 54 | theta = rev(seq(0, 2*pi, length.out=n)[-(n + 1)]) 55 | radius = runif(n, 10, 20) 56 | vertex.df = data.frame(x = radius*cos(theta), y = radius*sin(theta)) 57 | #' 58 | # Plot original polygon 59 | library(ggplot2) 60 | ggplot() + geom_polygon(data = vertex.df, aes(x = x, y = y))+coord_fixed() 61 | #' 62 | # Every corner is rounded with the same specified radius 63 | rounded.1 = round_polygon_corners(vertex.df, corner.radius = 1) 64 | ggplot() + geom_polygon(data = rounded.1, aes(x = x, y = y)) + coord_fixed() 65 | #' 66 | # The corners are rounded with individual, specified radius 67 | rounded.2 = round_polygon_corners(vertex.df, corner.radius = runif(n, 0, 1.9)) 68 | ggplot() + geom_polygon(data = rounded.2, aes(x = x, y = y)) + coord_fixed() 69 | #' 70 | # Every corner is rounded with the same optimal radius 71 | rounded.3 = round_polygon_corners(vertex.df, corner.radius = "constant") 72 | ggplot() + geom_polygon(data = rounded.3, aes(x = x, y = y)) + coord_fixed() 73 | #' 74 | # The corners are rounded with optimal individual radius 75 | rounded.4 = round_polygon_corners(vertex.df, corner.radius = "varying") 76 | ggplot() + geom_polygon(data = rounded.4, aes(x = x, y = y)) + coord_fixed() 77 | 78 | # Comparison of different options 79 | ggplot()+ 80 | geom_polygon(data = rounded.1, aes(x = x, y = y, fill = "1. Constant, manual"))+ 81 | geom_polygon(data = rounded.2, aes(x = x + 40, y = y, fill = "2. Varying, manual"))+ 82 | geom_polygon(data = rounded.3, aes(x = x, y = y - 40, fill = "3. Constant, optimal"))+ 83 | geom_polygon(data = rounded.4, aes(x = x + 40, y = y - 40, fill = "4. Varying, optimal"))+ 84 | coord_fixed() 85 | 86 | # Below we what happens if the corner radius is too large: 87 | # The circular arcs are not connected in a smooth way 88 | rounded.5 = round_polygon_corners(vertex.df, corner.radius = 4) 89 | ggplot() + geom_polygon(data = rounded.5, aes(x = x, y = y)) + coord_fixed() 90 | 91 | } 92 | \author{ 93 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 94 | } 95 | -------------------------------------------------------------------------------- /R/cut-polygons.R: -------------------------------------------------------------------------------- 1 | #' Repeated cutting of polygons 2 | #' 3 | #' This function takes in an arbitrary number of polygons and cuts them a given 4 | #' number of times. For each iteration, every polygon is cut by a randomly generated 5 | #' line, which divides it into several parts. 6 | #' 7 | #' @param vertex.df A data frame where each row corresponds to the vertex of a polygon. 8 | #' It must contain the columns \code{x}, \code{y}, and optionally \code{group} (must be integer). 9 | #' \code{x} and \code{y} specify the coordinates of the vertex, 10 | #' and \code{group} is used to indicate which polygon the vertex belongs to. 11 | #' If \code{vertex.df} contains only a single polygon, \code{group} can be omitted. 12 | #' @param number.of.iterations The number of times that the polygons are cut in 13 | #' half. Either a single value, or a numeric vector with one value per polygon in \code{vertex.df}. 14 | #' @param use.centroid If \code{TRUE}, the randomly generated line goes through the centroid 15 | #' of the polygon. Otherwise, the line goes through a point sampled uniformly 16 | #' from the interior of the polygon. 17 | #' @param seed The seed used when generating the directions of the cut lines. 18 | #' When no value is specified, the seed is not set. 19 | #' @param return.all If \code{TRUE}, the function will return a data frame 20 | #' that contains every iteration (i.e. the state after iteration 1,...,\code{number.of.iterations}). 21 | #' 22 | #' @note \code{number.of.iterations} should be chosen with care, as each 23 | #' iteration at least doubles the number of polygons, leading to exponential growth. 24 | #' 25 | #' @details The directions of the lines are sampled from a uniform distribution. 26 | #' 27 | #' @return A data frame where each row corresponds to the vertex of a polygon, 28 | #' with the following columns: 29 | #' \item{x, y}{The coordinates of the vertex} 30 | #' \item{group}{The index of the polygon that the vertex belongs to} 31 | #' \item{iteration}{The iteration that the polygon belongs to (only present when \code{return.all = TRUE})} 32 | #' 33 | #' @examples 34 | #' # Example with single initial polygon (square) 35 | #' vertex.df = data.frame( 36 | #' x = c(-1, 1, 1, -1), 37 | #' y = c(1, 1, -1, -1) 38 | #' ) 39 | #' cut.df = cut_polygons(vertex.df, number.of.iterations = 10, seed = 123) 40 | #' 41 | #' library(ggplot2) 42 | #' ggplot()+ 43 | #' geom_polygon(data = cut.df, 44 | #' aes(x = x, y = y, group = group), 45 | #' fill = NA, color = "black")+ 46 | #' coord_fixed() 47 | #' 48 | #' # Example with multiple initial polygons (square and triangle) 49 | #' vertex.df = data.frame( 50 | #' x = c(-1, 1, 1, -1, 1, -1, 0), 51 | #' y = c(1, 1, -1, -1, 2, 2, 2+sqrt(3)), 52 | #' group = c(rep(1, 4), rep(2, 3)) 53 | #' ) 54 | #' 55 | #' # Cut square and triangle 10 and 9 times, respectively 56 | #' cut.df = cut_polygons(vertex.df, number.of.iterations = c(10, 9), seed = 123) 57 | #' 58 | #' ggplot()+ 59 | #' geom_polygon(data = cut.df, 60 | #' aes(x = x, y = y, group = group), 61 | #' fill = NA, color = "black")+ 62 | #' coord_fixed() 63 | #' 64 | #' @export 65 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 66 | cut_polygons = function(vertex.df, number.of.iterations, use.centroid = TRUE, seed = NULL, return.all = FALSE) { 67 | check_vertex_df(vertex.df) 68 | 69 | if (!is.null(seed)) { 70 | set.seed(seed) 71 | } 72 | 73 | cut_function = if(return.all) cutEveryPolygonReturnAllRcpp else cutEveryPolygonRcpp 74 | 75 | if (!contains_columns(vertex.df, "group")) { 76 | vertex.df$group = 1 77 | } 78 | 79 | groups = unique(vertex.df$group) 80 | num.groups = length(groups) 81 | 82 | if (length(number.of.iterations) == 1) { 83 | cut.df = cut_function(vertex.df$x, vertex.df$y, vertex.df$group, number.of.iterations, use.centroid) 84 | } else if (length(number.of.iterations) == num.groups) { 85 | cut.df = data.frame() 86 | max.group = 0 87 | for (i in 1:num.groups) { 88 | group.df = vertex.df[vertex.df$group == groups[i], ] 89 | current.df = cut_function(group.df$x, group.df$y, group.df$group, number.of.iterations[i], use.centroid) 90 | current.df$group = current.df$group + max.group 91 | cut.df = rbind(cut.df, current.df) 92 | max.group = max(current.df$group) 93 | } 94 | } else { 95 | stop("number.of.iterations must either be a single integer or ", 96 | "a numeric vector containing one integer per group in vertex.df.") 97 | } 98 | return(cut.df) 99 | } 100 | -------------------------------------------------------------------------------- /R/rotate-polygon.R: -------------------------------------------------------------------------------- 1 | #' Rotate polygons 2 | #' 3 | #' Takes in one or more polygons and rotates each one a specified amount. 4 | #' 5 | #' @inheritParams cut_polygons 6 | #' @param rotation The amount each polygon is to be rotated in radians. Either a single 7 | #' number or a vector containing one number per polygon in \code{vertex.df}. 8 | #' @param center.of.rotation The center of rotation, i.e. the point which the polygons 9 | #' will be rotated around. Can be one of the following: 10 | #' \itemize{ 11 | #' \item{"centroid" (default), where each polygon is rotated around its centroid} 12 | #' \item{A 1 x 2 matrix data frame/matrix that specifices the x- and y-coordinate 13 | #' of the center of rotation used for every polygon (a numeric vector of length 2 14 | #' is also accepted, i.e. c(x, y))} 15 | #' \item{A n x 2 data frame/matrix, where row number i contains the x- and y-coordinates of 16 | #' the center of rotation for polygon number i in \code{vertex.df}, and n is the number of polygons.} 17 | #' } 18 | #' 19 | #' @return The returned data frame is equal to \code{vertex.df} everywhere, except 20 | #' the \code{x}- and \code{y}-columns that are changed due to the rotation. 21 | #' 22 | #' @examples 23 | #' polygon.center = data.frame(x = c(0, 2, 4), y = c(0, 2, 4)) 24 | #' original.df = compute_regular_polygons( 25 | #' center = polygon.center, 26 | #' radius = c(1, 1, 1.5), 27 | #' num.edges = 3:5 28 | #' ) 29 | #' 30 | #' # Rotating every polygon the same amount, using the centroids as centers of rotation 31 | #' centroid.df = rotate_polygon(original.df, rotation = pi/8, center.of.rotation = "centroid") 32 | #' # Rotating each polygon a different amount, around a different center of rotation 33 | #' center.of.rotation = data.frame(x = c(2, 0, 4), y = c(0, 2, 0)) 34 | #' manual.df = rotate_polygon( 35 | #' original.df, 36 | #' rotation = c(pi/4, pi, -pi/2), 37 | #' center.of.rotation = center.of.rotation 38 | #' ) 39 | #' 40 | #' # The plot below shows both the original and rotated polygons. 41 | #' # The black lines connect the center of rotation used in manual.df to the centroids 42 | #' # of the original polygons. 43 | #' library(ggplot2) 44 | #' ggplot()+ 45 | #' geom_polygon(data = original.df, aes(x = x, y = y, group = group, color = "1. Original"), 46 | #' fill = NA)+ 47 | #' geom_polygon(data = centroid.df, aes(x = x, y = y, group = group, color = "2. Centroid"), 48 | #' fill = NA)+ 49 | #' geom_polygon(data = manual.df, aes(x = x, y = y, group = group, color = "3. Manual"), 50 | #' fill = NA)+ 51 | #' geom_segment(aes(x = polygon.center$x, xend = center.of.rotation$x, 52 | #' y = polygon.center$y, yend = center.of.rotation$y))+ 53 | #' scale_color_manual("", values = c("red", "green", "blue"))+ 54 | #' coord_fixed() 55 | #' 56 | #' 57 | #' @export 58 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 59 | rotate_polygon = function(vertex.df, rotation, center.of.rotation = "centroid") { 60 | check_vertex_df(vertex.df) 61 | n = nrow(vertex.df) 62 | 63 | if (!contains_columns(vertex.df, "group")) { 64 | vertex.df$group = 1 65 | } 66 | 67 | group.count = as.numeric(table(vertex.df$group)) 68 | num.groups = length(unique(vertex.df$group)) 69 | 70 | if (length(rotation) == 1) { 71 | rotation = rep(rotation, num.groups) 72 | } else if (length(rotation) != num.groups) { 73 | stop("\"rotation\" must either be a single number or a numeric vector containing ", 74 | "one number per group in vertex.df.") 75 | } 76 | 77 | dims = dim(center.of.rotation) 78 | uses.centroids = FALSE 79 | if (!is.null(dims)) { 80 | if (dims[2] != 2) { 81 | stop("Invalid value for \"center.of.rotation\". See ?rotate_polygon for valid formats.") 82 | } else if (dims[1] == 1) { 83 | center = center.of.rotation[rep(1, num.groups), ] 84 | } else if (dims[1] == num.groups) { 85 | center = center.of.rotation 86 | } else { 87 | stop("Invalid value for \"center.of.rotation\". See ?rotate_polygon for valid formats.") 88 | } 89 | } else if (is.null(dims) && length(center.of.rotation) == 2) { 90 | center = matrix(center.of.rotation, nrow = num.groups, ncol = 2, byrow = TRUE) 91 | } else if (center.of.rotation == "centroid") { 92 | center = compute_polygon_centroid(vertex.df) 93 | uses.centroids = TRUE 94 | } else { 95 | stop("Invalid value for \"center.of.rotation\". See ?rotate_polygon for valid formats.") 96 | } 97 | 98 | if (!uses.centroids) { 99 | colnames(center) = c("x", "y") 100 | } 101 | 102 | rotation.total = rep(rotation, group.count) 103 | center.total = center[rep(1:num.groups, group.count), ] 104 | 105 | rotated.df = vertex.df 106 | rotated.df[, "x"] = center.total[, "x"] + 107 | (vertex.df[, "x"] - center.total[, "x"])*cos(rotation.total) - 108 | (vertex.df[, "y"] - center.total[, "y"])*sin(rotation.total) 109 | rotated.df[, "y"] = center.total[, "y"] + 110 | (vertex.df[, "x"] - center.total[, "x"])*sin(rotation.total) + 111 | (vertex.df[, "y"] - center.total[, "y"])*cos(rotation.total) 112 | return(rotated.df) 113 | } 114 | -------------------------------------------------------------------------------- /R/compute-regular-polygons.R: -------------------------------------------------------------------------------- 1 | #' Compute regular polygons 2 | #' 3 | #' This function computes one or more regular polygons. 4 | #' 5 | #' @param center Specifies the center(s) of the polygon(s). Can either be a 6 | #' numeric vector of length 2 (every polygon has the same center), or a 7 | #' data frame/matrix with 1 row per polygon. If the data frame/matrix has 8 | #' columns named \code{x} and \code{y}, these are used as center coordinates. 9 | #' Otherwise, the first two columns are used. 10 | #' @param radius The radius of the circumscribed circle of the polygon. Can either 11 | #' be a single number or a vector containing one radius per polygon. 12 | #' @param rotation The amount of rotation applied to the polygon(s), in radians. 13 | #' If \code{rotation = 0}, then the first vertex is at the "top" of the polygon. 14 | #' Can either be a single number or a vector containing a different rotation per polygon. 15 | #' @param num.edges The number of edges that the polygon(s) contain. Can either 16 | #' be a single integer or a vector containing a different number of edges per polygon. 17 | #' @param include.info If true, the returned data frame will contain the values for 18 | #' \code{radius}, \code{rotation} and \code{num.edges} as columns. 19 | #' @param ... Custom properties that can be used to store additional information about 20 | #' the polygon(s), such as color and opacity. Can either be a single value or a 21 | #' vector containing a different value per polygon. 22 | #' 23 | #' @return A data frame where each row corresponds to the vertex of a polygon. 24 | #' The following columns are included: 25 | #' \item{x, y}{The coordinates of the vertex} 26 | #' \item{group}{The group number of the polygon} 27 | #' \item{radius, rotation, num.edges}{ The parameters that are specified in the function call. 28 | #' Only included if \code{include.info = TRUE}} 29 | #' \item{...}{Custom columns that are specified in the function call.} 30 | #' 31 | #' @examples 32 | #' set.seed(123) 33 | #' # This generates 10 polygons with different centers, an individual number of edges 34 | #' # and a custom property "color" 35 | #' vertex.df = compute_regular_polygons( 36 | #' center = data.frame(x = 1:10, y = 1:10), 37 | #' radius = 0.5, 38 | #' num.edges = 3:12, 39 | #' color = sample(c("red", "blue", "green"), 10, replace = TRUE) 40 | #' ) 41 | #' 42 | #' library(ggplot2) 43 | #' ggplot()+ 44 | #' geom_polygon(data = vertex.df, aes(x = x, y = y, group = group, fill = color))+ 45 | #' scale_fill_identity()+ 46 | #' coord_fixed() 47 | #' 48 | #' 49 | #' @export 50 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 51 | compute_regular_polygons = function(center = c(0, 0), radius = 1, rotation = 0, num.edges = 50, include.info = TRUE, ...) { 52 | if (contains_columns(center, c("x", "y"))) { 53 | x = center[, "x"] 54 | y = center[, "y"] 55 | } else if (length(dim(center)) > 1 && ncol(center) == 2) { 56 | x = center[, 1] 57 | y = center[, 2] 58 | } else if (is.null(dim(center)) && is.numeric(center) && length(center) == 2) { 59 | x = center[1] 60 | y = center[2] 61 | } else { 62 | stop('center must either be a numeric vector of length 2, or a data ', 63 | 'frame/matrix that contains 2 columns or has columns named "x" and "y".') 64 | } 65 | 66 | if (any(radius < 0)) { 67 | warning("radius contains negative elements.") 68 | } 69 | 70 | if (any(num.edges < 3)) { 71 | warning("num.edges contains elements less than 3.") 72 | } 73 | 74 | properties = c(list(radius = radius, rotation = rotation, num.edges = num.edges), list(...)) 75 | n = max(length(x), sapply(properties, length)) 76 | 77 | if (length(x) == 1) { 78 | x = rep(x, n) 79 | y = rep(y, n) 80 | } else if (length(x) != n) { 81 | stop("center must contain either a single point, or one point per polygon.") 82 | } 83 | 84 | 85 | for (name in names(properties)) { 86 | if (length(properties[[name]]) == 1) { 87 | properties[[name]] = rep(properties[[name]], n) 88 | } else if (length(properties[[name]]) != n) { 89 | stop(sprintf('%s must contain either a single value, or one value per polygon.', name)) 90 | } 91 | } 92 | 93 | total.edges = sum(properties$num.edges) 94 | # Vector that contains polar coordinate angles for each polygon. Add pi/2 to make first vertex on top 95 | theta.total = 2*pi*(0:(total.edges - 1) - rep(cumsum(c(0, properties$num.edges))[1:n], properties$num.edges)) / 96 | rep(properties$num.edges, properties$num.edges) + pi/2 97 | 98 | x.total = rep(x, properties$num.edges) 99 | y.total = rep(y, properties$num.edges) 100 | radius.total = rep(properties$radius, properties$num.edges) 101 | rotation.total = rep(properties$rotation, properties$num.edges) 102 | theta.rotation.total = theta.total + rotation.total 103 | group.total = rep(1:n, properties$num.edges) 104 | 105 | result.df = data.frame( 106 | x = x.total + radius.total*cos(theta.rotation.total), 107 | y = y.total + radius.total*sin(theta.rotation.total), 108 | group = group.total 109 | ) 110 | if (include.info) { 111 | for (name in names(properties)) { 112 | result.df[[name]] = rep(properties[[name]], properties$num.edges) 113 | } 114 | } else { 115 | dot.properties = list(...) 116 | for (name in names(dot.properties)) { 117 | result.df[[name]] = rep(dot.properties[[name]], properties$num.edges) 118 | } 119 | } 120 | return(result.df) 121 | } 122 | 123 | -------------------------------------------------------------------------------- /man/interpolate_polygon.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/interpolate-polygon.R 3 | \name{interpolate_polygon} 4 | \alias{interpolate_polygon} 5 | \title{Interpolation of polygon boundary} 6 | \usage{ 7 | interpolate_polygon(vertex.df, method = "linear") 8 | } 9 | \arguments{ 10 | \item{vertex.df}{A data frame where each row corresponds to a vertex in the polygon. 11 | It must contain the columns \code{x} and \code{y} where \code{x} and \code{y} 12 | specify the coordinates of the vertex,} 13 | 14 | \item{method}{Can be either "linear" (linear interpolation) or "spline" (periodic 15 | cubic spline interpolation). When "linear" is used, the shape traced out by 16 | the interpolation function has the same shape as the original polygon, and the 17 | edges remain straight. With "spline", the interpolation function is guaranteed 18 | to go through the vertices of the original polygon, but the shape will be smooth 19 | (i.e. no straight edges and sharp corners).} 20 | } 21 | \value{ 22 | The return value is a function that interpolates along the boundary 23 | of the polygon. It takes in two parameters: 24 | \item{time.values}{A single value or a vector of n values between 0 and 1, where 25 | 0 corresponds to the position of the first vertex, 0.5 gives the position exactly halfway 26 | around the perimeter, and 1 is the "end" of the polygon (which is back at the first vertex).} 27 | \item{original.vertices}{Can either be "ignore", "replace" or "add". If "ignore", 28 | the interpolation function is evaluated in the exact values given in \code{time.values}. 29 | If "replace", some of the values in \code{time.values} will be replaced so that 30 | the returned vertices are guaranteed to contain the original vertices. With "add", 31 | the original vertices are added inbetween the vertices obtained with \code{time.values}.} 32 | The function returns an n x 2 (plus some extra rows when \code{original.vertices = add}) data frame that contains the x- and y-coordinates of 33 | the interpolated vertices. 34 | } 35 | \description{ 36 | This function takes in a single polygon and returns a function that interpolates 37 | the boundary of the polygon. This is useful for "filling in" 38 | } 39 | \details{ 40 | When \code{method = "linear"}, the interpolation function has the nice 41 | property that evenly distributed inputs will lead to vertices that are evenly distributed 42 | along the boundary of the polygon (i.e. the distance between consecutive vertices is constant). 43 | In other words: If L is the perimeter of the original polygon, then the arc length 44 | traced by the interpolation function from time 0 to t is L*t. 45 | } 46 | \note{ 47 | The results obtained with \code{method = "spline"} are generally unpredictable, 48 | such shapes that are self-intersecting, or perfect circles when the input is a regular polygon. 49 | This option should therefore be used with caution. If the goal is to get a 50 | smoothed version of the polygon, \code{\link{round_polygon_corners}} might be a better choice. 51 | } 52 | \examples{ 53 | # Example: Interpolation of pentagon and comparison of original.vertices 54 | # Create pentagon using compute_regular_polygons 55 | vertex.df = compute_regular_polygons(center = c(0, 0), radius = 1, num.edges = 5) 56 | # How the pentagon looks: 57 | library(ggplot2) 58 | ggplot()+ 59 | geom_polygon(data = vertex.df, aes(x = x, y = y), fill = "pink")+ 60 | geom_point(data = vertex.df, aes(x = x, y = y), color = "black")+ 61 | coord_fixed() 62 | # Get boundary interpolation function from interpolate_polygon 63 | interpolation.function = interpolate_polygon(vertex.df) 64 | # The number of vertices that we want to end up with 65 | num.interp = 18 66 | # Create 18 evenly distributed values between 0 and 1. Remove last element, 67 | # as 1 leads to same position as 0 68 | time.interp = seq(0, 1, length.out = num.interp + 1)[-(num.interp + 1)] 69 | 70 | # One data frame per value of original.vertices 71 | ignore.df = interpolation.function(time.interp, original.vertices = "ignore") 72 | replace.df = interpolation.function(time.interp, original.vertices = "replace") 73 | add.df = interpolation.function(time.interp, original.vertices = "add") 74 | 75 | # Comparison of interpolated vertices with different original.vertices: 76 | ggplot()+ 77 | geom_polygon(data = ignore.df, aes(x = x, y = y, fill = "1. ignore"))+ 78 | geom_point(data = ignore.df, aes(x = x, y = y), color = "black")+ 79 | geom_polygon(data = replace.df, aes(x = x + 2, y = y, fill = "2. replace"))+ 80 | geom_point(data = replace.df, aes(x = x + 2, y = y), color = "black")+ 81 | geom_polygon(data = add.df, aes(x = x + 4, y = y, fill = "3. add"))+ 82 | geom_point(data = add.df, aes(x = x + 4, y = y), fill = NA, color = "black")+ 83 | scale_fill_manual("", values = c("red", "green", "blue"))+ 84 | coord_fixed() 85 | # When original.vertices = "ignore", the interpolated vertices contain only the 86 | # first vertex from the original polygon, which leads to a different shape. 87 | # The vertices are, however, evenly distributed (i.e. the distance between consecutive 88 | # vertices is constant). With "replace", the interpolated vertices contain all 89 | # of the original vertices, but they are not evenly distributed along 90 | # the boundary. The same holds for "add", where the interpolated vertices 91 | # contains both the vertices shown in "ignore" and the original vertices. 92 | # Note: These differences are very noticeable since num.interp is small. 93 | # Usually, we will use more than 18 interpolated vertices. 94 | 95 | } 96 | \author{ 97 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 98 | } 99 | -------------------------------------------------------------------------------- /R/transition-between-polygons.R: -------------------------------------------------------------------------------- 1 | #' Transition between polygons 2 | #' 3 | #' Function that interpolates/transitions between two polygons. This is done by 4 | #' computing \code{(1 - time)*start.polygon + time*end.polygon}. 5 | #' 6 | #' @param start.polygon,end.polygon Dataframes/matrices containing the vertices of the 7 | #' polygons to interpolate between. The coordinates of the vertices must be stored in columns named "x" and "y", 8 | #' and the polygons must contain the same number of vertices. 9 | #' @param time The times at which we are interested in interpolating the polygons. \code{time = 0} gives 10 | #' \code{start.polygon}, while \code{time = 1} gives \code{end.polygon}. Can be either a single number 11 | #' or a numeric vector. If time contains values outside [0, 1], a warning will be given. 12 | #' @param vertex.order Determines whether and how the vertices in \code{start.polygon} and \code{end.polygon} 13 | #' are reordered before the interpolation is computed. If \code{vertex.order = "preserve"}, no 14 | #' reordering is applied, and the polygons are used as-is. If \code{vertex.order = "reorder"}, 15 | #' the function first ensures that the polygons have the same orientation (i.e. clockwise/counter-clockwise). 16 | #' Then, it attempts to shift the indices of the vertices so that the corresponding vertices on 17 | #' \code{start.polygon} and \code{end.polygon} are "aligned". 18 | #' 19 | #' @return A data frame that contains one row per vertex. If \code{start.polygon} 20 | #' and \code{end.polygon} contain n vertices, and \code{time} contains m values, then 21 | #' the returned data frame will contain n*m rows. following columns: 22 | #' \item{x, y}{The coordinates of the vertex} 23 | #' \item{group}{Which polygon the vertex belongs to (1 for the first value in \code{time}, 2 for the second and so on)} 24 | #' \item{time}{The time value of the associated polygon} 25 | #' 26 | #' @note It is recommended to ensure that the start and end polygons have the correct 27 | #' orientation and numbering of vertices before computing the transition, and then using 28 | #' \code{vertex.order = "preserve"}. 29 | #' 30 | #' @examples 31 | #' # Example: Transition from hexagon to square 32 | #' # Create hexagon 33 | #' hexagon.df = compute_regular_polygons( 34 | #' center = c(0, 0), 35 | #' radius = 1, 36 | #' rotation = 0, 37 | #' num.edges = 6 38 | #' ) 39 | #' # Round corners slightly 40 | #' hexagon.df = round_polygon_corners(hexagon.df, corner.radius.scale = 0.3) 41 | #' 42 | #' # Create square 43 | #' square.df = compute_regular_polygons( 44 | #' center = c(20, -20), 45 | #' radius = 2, 46 | #' rotation = 0, 47 | #' num.edges = 4 48 | #' ) 49 | #' # Round corners slightly 50 | #' square.df = round_polygon_corners(square.df, corner.radius.scale = 0.3) 51 | #' 52 | #' # Resample polygons with many vertices, so that the transition becomes smooth 53 | #' num.vertices = 1000 54 | #' resample.time = seq(0, 1, length.out = num.vertices + 1)[-(num.vertices + 1)] 55 | #' hexagon.resample = interpolate_polygon(hexagon.df)(resample.time) 56 | #' square.resample = interpolate_polygon(square.df)(resample.time) 57 | #' 58 | #' # Show transition over 10 steps 59 | #' num.transition = 10 60 | #' transition.time = seq(0, 1, length.out = num.transition) 61 | #' # Use vertex.order = "preserve" (both polygons are CCW, and have the top vertex 62 | #' # as the first in hexagon.df and square.df) 63 | #' transition.df = transition_between_polygons( 64 | #' hexagon.resample, 65 | #' square.resample, 66 | #' transition.time, 67 | #' "preserve") 68 | #' 69 | #' # Show the result: 70 | #' library(ggplot2) 71 | #' ggplot()+ 72 | #' geom_polygon(data = transition.df, aes(x = x, y = y, group = group), fill = NA, color = "black")+ 73 | #' coord_fixed() 74 | #' 75 | #' @export 76 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 77 | transition_between_polygons = function(start.polygon, end.polygon, time, vertex.order = "reorder") { 78 | check_vertex_df(start.polygon, "start.polygon") 79 | check_vertex_df(end.polygon, "end.polygon") 80 | 81 | if (nrow(start.polygon) != nrow(end.polygon)) { 82 | stop(paste(c("start.polygon and end.polygon must contain the same number of vertices. ", 83 | "If you wish to interpolate between polygons of different sizes, ", 84 | "then interpolate_polygon can be used to make them the same size."))) 85 | } 86 | 87 | if (min(time) < 0 || max(time) > 1) { 88 | warning("time contains values outside the interval [0, 1].") 89 | } 90 | n = nrow(start.polygon) 91 | n.times = length(time) 92 | 93 | if (is_polygon_ccw(start.polygon) != is_polygon_ccw(end.polygon)) { 94 | if (vertex.order != "preserve") { 95 | end.polygon = end.polygon[n:1, ] 96 | } else { 97 | warning(paste(c("start.polygon and end.polygon do not have the same orientation ", 98 | "(one is clockwise and the other is counter-clockwise.)"))) 99 | } 100 | } 101 | 102 | if (vertex.order == "reorder") { 103 | reordered.polygons = reorder_polygons(start.polygon, end.polygon) 104 | start.polygon = reordered.polygons$polygon.1 105 | end.polygon = reordered.polygons$polygon.2 106 | } 107 | 108 | start.polygon.total = start.polygon[rep(1:n, n.times), c("x", "y")] 109 | end.polygon.total = end.polygon[rep(1:n, n.times), c("x", "y")] 110 | time.total = rep(time, each = n) 111 | group = rep(1:n.times, each = n) 112 | 113 | result = (1 - time.total)*start.polygon.total + time.total*end.polygon.total 114 | result$time = time.total 115 | result$group = group 116 | return(result) 117 | } 118 | 119 | reorder_polygons = function(polygon.1, polygon.2) { 120 | n = nrow(polygon.1) 121 | centroid.1 = compute_polygon_centroid(polygon.1) 122 | centroid.2 = compute_polygon_centroid(polygon.2) 123 | 124 | centered.polygon.1 = polygon.1[, c("x", "y")] - centroid.1[rep(1, n), c("x", "y")] 125 | centered.polygon.2 = polygon.2[, c("x", "y")] - centroid.2[rep(1, n), c("x", "y")] 126 | 127 | closest.indices = closest_pair_of_points(centered.polygon.1, centered.polygon.2) 128 | reordered.indices.start = (1:n + closest.indices[1] - 2) %% n + 1 129 | reordered.indices.end = (1:n + closest.indices[2] - 2) %% n + 1 130 | 131 | reordered.polygon.1 = polygon.1[reordered.indices.start, ] 132 | reordered.polygon.2 = polygon.2[reordered.indices.start, ] 133 | return(list(polygon.1 = reordered.polygon.1, polygon.2 = reordered.polygon.2)) 134 | } 135 | 136 | -------------------------------------------------------------------------------- /R/image-from-function.R: -------------------------------------------------------------------------------- 1 | #' Create image based on color function 2 | #' 3 | #' This function takes in a function that maps every location over some rectangular 4 | #' area to a color, and saves a PNG image based on the output of the function. 5 | #' 6 | #' @param filename The filename of the output PNG file. It does not need to end 7 | #' in ".png". 8 | #' @param color.function A function that takes in an n x 2 data frame of locations 9 | #' inside the rectangle specified in \code{bounds}, and returns one color per location. 10 | #' The data frame has columns named \code{x} and \code{y}. 11 | #' The function can return the colors in one of the following formats: 12 | #' \itemize{ 13 | #' \item{A vector of n hex codes, e.g. "#FF0000" or "#123456AB" (where "AB" in the latter is alpha / transparency)} 14 | #' \item{A vector of n greyscale values between 0 and 1, where 0 is black and 1 is white} 15 | #' \item{An n x 2 matrix of values between 0 and 1, where the first column is the grayscale value and the second is alpha} 16 | #' \item{An n x 3 matrix of values between 0 and 1, where the columns correspond to R, G and B} 17 | #' \item{An n x 4 matrix of values between 0 and 1, where the columns correspond to R, G, B and alpha} 18 | #' } 19 | #' @param bounds A four-element vector that specifies the rectangular area used 20 | #' when evaluating \code{color.function}. The first two elements are the lower and 21 | #' upper bounds for the x-coordinate, while the last two are the lower and upper 22 | #' bounds for the y-coordinate. 23 | #' @param width,height The number of columns and rows in the output image, so that 24 | #' the total number of pixels is \code{width*height}. 25 | #' @param num.portions When this parameter is greater than 1, the output image 26 | #' will be divided into \code{num.portions} vertical slices of approximately 27 | #' the same height. This is useful for reducing memory use when \code{width} and 28 | #' \code{height} are large. The slices can be merged into the original image with 29 | #' image editing software. 30 | #' 31 | #' @note The aspect ratio of the output image (\code{width / height}) should be 32 | #' the same as the aspect ratio of \code{bounds}. If not, a warning is issued. 33 | #' 34 | #' @examples 35 | #' \dontrun{ 36 | #' # Example with hex values 37 | #' color.function.hex = function(locations) { 38 | #' colors = c("#df361f", "#21747a", "#edd324") 39 | #' return(sample(colors, size = nrow(locations), replace = TRUE)) 40 | #' } 41 | #' 42 | #' image_from_function("hex.png", color.function.hex, 43 | #' width = 1000, height = 1000) 44 | #' 45 | #' # Example with grayscale 46 | #' color.function.gray = function(locations) { 47 | #' gray = minmax_scaling(locations$x^2 + locations$y^2) 48 | #' return(gray) 49 | #' } 50 | #' 51 | #' image_from_function("grayscale.png", color.function.gray, 52 | #' width = 1000, height = 1000) 53 | #' 54 | #' 55 | #' # Example with RGB matrix 56 | #' color.function.rgb = function(locations) { 57 | #' red = minmax_scaling(locations$x^2) 58 | #' green = minmax_scaling(sin(pi*locations$y), 0, 0.5) 59 | #' blue = minmax_scaling(locations$x*locations$y) 60 | #' return(cbind(red, green, blue)) 61 | #' } 62 | #' 63 | #' image_from_function("rgb.png", color.function.rgb, bounds = c(-2, 2, -1, 1), 64 | #' width = 2000, height = 1000) 65 | #' 66 | #' # Example with RGBA matrix 67 | #' color.function.rgba = function(locations) { 68 | #' red = minmax_scaling(sin(pi*locations$x)) 69 | #' green = minmax_scaling(sin(2*pi*locations$x + 0.25)) 70 | #' blue = minmax_scaling(sin(4*pi*locations$x + 0.5)) 71 | #' alpha = minmax_scaling(sin(6*pi*locations$x + 0.75)) 72 | #' return(cbind(red, green, blue, alpha)) 73 | #' } 74 | #' 75 | #' image_from_function("rgba.png", color.function.rgba, 76 | #' width = 1000, height = 1000) 77 | #'} 78 | #' @export 79 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 80 | image_from_function = function(filename, color.function, bounds = c(-1, 1, -1, 1), width, height, num.portions = 1) { 81 | fn.split = strsplit(x = filename, split = "[.]")[[1]] 82 | if (length(fn.split) > 1 && tolower(fn.split[length(fn.split)]) == "png") { 83 | filename = substr(filename, 1, nchar(filename) - 4) 84 | } 85 | 86 | ratio.1 = width/height 87 | ratio.2 = (bounds[2] - bounds[1])/(bounds[4] - bounds[3]) 88 | if (abs(ratio.1 - ratio.2) > 10^(-5)) { 89 | warning('The aspect ratio of the output (width/height = ', round(ratio.1, 5), ') is not equal to ', 90 | 'the aspect ratio of bounds (', round(ratio.2, 5), ')') 91 | } 92 | 93 | if (num.portions < 1) { 94 | stop("num.portions must be greater than or equal to 1.") 95 | } 96 | 97 | if (width < 1 || height < 1) { 98 | stop("Both width and height must be greater than 0.") 99 | } 100 | 101 | portion.rows = round(seq(0, height, length.out = num.portions+1)) 102 | portion.size = diff(portion.rows) 103 | 104 | height.portions = bounds[3] + portion.rows/height*(bounds[4] - bounds[3]) 105 | 106 | for (i in 1:num.portions) { 107 | num.selected.rows = portion.size[i] 108 | # Let height.portions[i+1] and height.portions[i] be lower and upper to 109 | # make y increase upwards in output 110 | pixel.locations = generate_grid_centers(bounds[1], bounds[2], height.portions[i+1], height.portions[i], 111 | width, num.selected.rows) 112 | 113 | color.values = color.function(pixel.locations) 114 | color.matrix = handle_color_output(color.values) 115 | 116 | if (any(color.matrix < 0 || color.matrix > 1)) { 117 | warning("color.function returns colors values outside the interval [0, 1].") 118 | } 119 | 120 | output.array = array(dim = c(num.selected.rows, width, ncol(color.matrix))) 121 | for (j in 1:ncol(color.matrix)) { 122 | output.array[, , j] = matrix(color.matrix[, j], nrow = num.selected.rows, ncol = width, byrow = TRUE) 123 | } 124 | current.filename = ifelse(num.portions > 1, paste(filename, i, sep = "-"), filename) 125 | png::writePNG(image = output.array, target = paste(current.filename, ".png", sep = "")) 126 | } 127 | } 128 | 129 | handle_color_output = function(color_output) { 130 | dims = dim(color_output) 131 | 132 | if (!is.null(dims)) { 133 | result = color_output 134 | } else if (is.numeric(color_output[1])) { 135 | result = matrix(color_output, ncol = 1) 136 | } else if (is.character(color_output[1])) { 137 | result = t(grDevices::col2rgb(color_output, alpha = TRUE)) / 255 138 | } else { 139 | stop("Return type from color.function is invalid. See ?image-from-function for ", 140 | "for valid return formats.") 141 | } 142 | return(result) 143 | } 144 | -------------------------------------------------------------------------------- /man/deform_polygon.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deform-polygon.R 3 | \name{deform_polygon} 4 | \alias{deform_polygon} 5 | \title{Deform polygon} 6 | \usage{ 7 | deform_polygon(vertex.df, deform.distance, method = "bisector") 8 | } 9 | \arguments{ 10 | \item{vertex.df}{A data frame where each row corresponds to the vertex of a polygon. 11 | It must contain the columns \code{x}, \code{y}, and optionally \code{group} (must be integer). 12 | \code{x} and \code{y} specify the coordinates of the vertex, 13 | and \code{group} is used to indicate which polygon the vertex belongs to. 14 | If \code{vertex.df} contains only a single polygon, \code{group} can be omitted.} 15 | 16 | \item{deform.distance}{The distance that each vertex is to be moved. Can 17 | either be a single value or a vector containing one value per vertex. 18 | If the value is positive, the vertex is moved "outwards", while a negative 19 | value moves the vertex "inwards".} 20 | 21 | \item{method}{Must be one of the following values: 22 | \itemize{ 23 | \item{"bisector", where the vertex is moved along the bisector of the two adjacent edges} 24 | \item{"centroid", where the vertex is moved along the line between the vertex 25 | and the centroid of the polygon} 26 | }} 27 | } 28 | \value{ 29 | The same data frame that is given in \code{vertex.df}, the only difference 30 | being that the \code{x}- and \code{y}-columns are changed due to the deformation. 31 | } 32 | \description{ 33 | This function deforms a polygon by moving each vertex a specified distance. 34 | If you want a smooth deformation of a polygon with few vertices (e.g. a square), 35 | then \code{\link{interpolate_polygon}} can be used to create an interpolated version 36 | of the polygon (see example below). 37 | } 38 | \note{ 39 | This function is not robust, and can lead to strange results. While 40 | \code{method = "bisector"} generally looks better, it struggles in certain 41 | situations (see example #3 below), and should only be used for expanding convex 42 | polygons. If the polygon is concave and \code{deform.distance} contains 43 | both positive and negative values, \code{method = "centroid"} might be a 44 | better choice. 45 | } 46 | \examples{ 47 | # Example 1: Deformation of rounded square 48 | # Start with a square 49 | square.df = data.frame(x = c(-1, 1, 1, -1), y = c(1, 1, -1, -1)) 50 | # Round the corners of the square 51 | rounded.df = round_polygon_corners(square.df, corner.radius = 0.3) 52 | # The rounded polygon has many vertices along the corners, but none along the 53 | # straight edges. We need to resample! 54 | # Create function that allows us to resample vertices along the boundary of the polygon 55 | interpolation.function = interpolate_polygon(rounded.df) 56 | # How many vertices should resampled polygon contain? 57 | num.vertices = 1000 58 | # Create vector with n values evenly spaced between 0 and 1 59 | # (0 and 1 correspond to the same point, so the latter is removed) 60 | time = seq(0, 1, length.out = num.vertices + 1)[-(num.vertices + 1)] 61 | 62 | # Polygon that consists of 1000 vertices placed evenly along the boundary of 63 | # the rounded polygon 64 | resampled.df = interpolation.function(time) 65 | 66 | # Function that gives the amount of deformation for time between 0 and 1 67 | deform.function = function(t, freq) 0.1 + (0.35 - 0.1)*(1 + cos(freq*2*pi*t)) / 2 68 | # Vector that gives the amount of deformation for each vertex in resampled.df 69 | deform.amount = deform.function(time, 4) 70 | 71 | # Deformed polygon 72 | deform.df = deform_polygon(resampled.df, deform.amount, method = "bisector") 73 | 74 | library(ggplot2) 75 | ggplot()+ 76 | geom_polygon(data = square.df, aes(x = x, y = y, color = "1. Original"), fill = NA)+ 77 | geom_polygon(data = rounded.df, aes(x = x, y = y, color = "2. Rounded"), fill = NA)+ 78 | geom_polygon(data = deform.df, aes(x = x, y = y, color = "3. Deformed"), fill = NA)+ 79 | scale_color_manual(values = c("black", "blue", "red"))+ 80 | coord_fixed() 81 | 82 | # Example 2: Comparison of "bisector" and "centroid" methods 83 | # Start with a 2 x 10 rectangle 84 | rectangle.df = data.frame(x = c(-1, 1, 1, -1), y = c(5, 5, -5, -5)) 85 | 86 | # Resample polygon, this time without rounding 87 | interpolation.function = interpolate_polygon(rectangle.df) 88 | resampled.df = interpolation.function(time) 89 | 90 | # Deformation that oscillates faster than in example 1 91 | deform.amount = deform.function(time, 20) 92 | 93 | # Deformed polygons 94 | bisector.df = deform_polygon(resampled.df, deform.amount, method = "bisector") 95 | centroid.df = deform_polygon(resampled.df, deform.amount, method = "centroid") 96 | 97 | # From left to right, the plot shows the original rectangle, and the deformed polygons 98 | # from the "bisector" and "centroid" methods, respectively. 99 | # The result from "bisector" looks better overall, but struggles at sharp corners, 100 | # where the deformed polygon ends up having large gaps between the vertices. 101 | # This is avoided by rounding the polygon before deformation. 102 | # The deformed polygon from the "centroid" method handles the corners without problem, 103 | # but the amount of deformation is not consistent along the vertical sides. 104 | ggplot()+ 105 | geom_polygon(data = rectangle.df, aes(x = x, y = y), fill = "red")+ 106 | geom_polygon(data = bisector.df, aes(x = x + 3, y = y), fill = "red")+ 107 | geom_polygon(data = centroid.df, aes(x = x + 6, y = y), fill = "red")+ 108 | geom_point(data = bisector.df, aes(x = x + 3, y = y), color = "black", size = 0.5)+ 109 | geom_point(data = centroid.df, aes(x = x + 6, y = y), color = "black", size = 0.5)+ 110 | coord_fixed() 111 | 112 | # Example 3: Situation where "bisector" method fails 113 | set.seed(123) 114 | # Generate random polygon using polar coordinates 115 | num.vertices = 10 116 | theta = seq(0, 2*pi, length.out = num.vertices + 1)[-(num.vertices + 1)] 117 | radius = runif(num.vertices, 0.5, 1) 118 | vertex.df = data.frame(x = radius*cos(theta), y = radius*sin(theta)) 119 | # How the original polygon looks: 120 | ggplot()+ 121 | geom_polygon(data = vertex.df, aes(x = x, y = y), fill = NA, color = "black")+ 122 | coord_fixed() 123 | 124 | # Resample polygon before deforming 125 | interpolation.function = interpolate_polygon(vertex.df) 126 | resampled.df = interpolation.function(time) 127 | 128 | # Deform function goes from -0.2 to 0.2, and back to -0.2 129 | deform.function = function(t) -0.2*cos(2*pi*t) 130 | deform.amount = deform.function(time) 131 | 132 | # Deform resampled polygon 133 | deform.df = deform_polygon(resampled.df, deform.amount) 134 | 135 | # How the deformed polygon looks: 136 | ggplot()+ 137 | geom_polygon(data = deform.df, aes(x = x, y = y), fill = NA, color = "black")+ 138 | coord_fixed() 139 | # The combinations convex corner/negative deformation (right side of plot) 140 | # and concave corner/positive deformation (left side) leads to a shape that is 141 | # self-intersecting. 142 | } 143 | \author{ 144 | Mathias Isaksen \email{mathiasleanderi@gmail.com} 145 | } 146 | -------------------------------------------------------------------------------- /R/interpolate-polygon.R: -------------------------------------------------------------------------------- 1 | #' Interpolation of polygon boundary 2 | #' 3 | #' This function takes in a single polygon and returns a function that interpolates 4 | #' the boundary of the polygon. This is useful for "filling in" 5 | #' 6 | #' @param vertex.df A data frame where each row corresponds to a vertex in the polygon. 7 | #' It must contain the columns \code{x} and \code{y} where \code{x} and \code{y} 8 | #' specify the coordinates of the vertex, 9 | #' @param method Can be either "linear" (linear interpolation) or "spline" (periodic 10 | #' cubic spline interpolation). When "linear" is used, the shape traced out by 11 | #' the interpolation function has the same shape as the original polygon, and the 12 | #' edges remain straight. With "spline", the interpolation function is guaranteed 13 | #' to go through the vertices of the original polygon, but the shape will be smooth 14 | #' (i.e. no straight edges and sharp corners). 15 | #' 16 | #' @details When \code{method = "linear"}, the interpolation function has the nice 17 | #' property that evenly distributed inputs will lead to vertices that are evenly distributed 18 | #' along the boundary of the polygon (i.e. the distance between consecutive vertices is constant). 19 | #' In other words: If L is the perimeter of the original polygon, then the arc length 20 | #' traced by the interpolation function from time 0 to t is L*t. 21 | #' 22 | #' @note The results obtained with \code{method = "spline"} are generally unpredictable, 23 | #' such shapes that are self-intersecting, or perfect circles when the input is a regular polygon. 24 | #' This option should therefore be used with caution. If the goal is to get a 25 | #' smoothed version of the polygon, \code{\link{round_polygon_corners}} might be a better choice. 26 | #' 27 | #' @return The return value is a function that interpolates along the boundary 28 | #' of the polygon. It takes in two parameters: 29 | #' \item{time.values}{A single value or a vector of n values between 0 and 1, where 30 | #' 0 corresponds to the position of the first vertex, 0.5 gives the position exactly halfway 31 | #' around the perimeter, and 1 is the "end" of the polygon (which is back at the first vertex).} 32 | #' \item{original.vertices}{Can either be "ignore", "replace" or "add". If "ignore", 33 | #' the interpolation function is evaluated in the exact values given in \code{time.values}. 34 | #' If "replace", some of the values in \code{time.values} will be replaced so that 35 | #' the returned vertices are guaranteed to contain the original vertices. With "add", 36 | #' the original vertices are added inbetween the vertices obtained with \code{time.values}.} 37 | #' The function returns an n x 2 (plus some extra rows when \code{original.vertices = add}) data frame that contains the x- and y-coordinates of 38 | #' the interpolated vertices. 39 | #' 40 | #' @examples 41 | #' # Example: Interpolation of pentagon and comparison of original.vertices 42 | #' # Create pentagon using compute_regular_polygons 43 | #' vertex.df = compute_regular_polygons(center = c(0, 0), radius = 1, num.edges = 5) 44 | #' # How the pentagon looks: 45 | #' library(ggplot2) 46 | #' ggplot()+ 47 | #' geom_polygon(data = vertex.df, aes(x = x, y = y), fill = "pink")+ 48 | #' geom_point(data = vertex.df, aes(x = x, y = y), color = "black")+ 49 | #' coord_fixed() 50 | #' # Get boundary interpolation function from interpolate_polygon 51 | #' interpolation.function = interpolate_polygon(vertex.df) 52 | #' # The number of vertices that we want to end up with 53 | #' num.interp = 18 54 | #' # Create 18 evenly distributed values between 0 and 1. Remove last element, 55 | #' # as 1 leads to same position as 0 56 | #' time.interp = seq(0, 1, length.out = num.interp + 1)[-(num.interp + 1)] 57 | #' 58 | #' # One data frame per value of original.vertices 59 | #' ignore.df = interpolation.function(time.interp, original.vertices = "ignore") 60 | #' replace.df = interpolation.function(time.interp, original.vertices = "replace") 61 | #' add.df = interpolation.function(time.interp, original.vertices = "add") 62 | #' 63 | #' # Comparison of interpolated vertices with different original.vertices: 64 | #' ggplot()+ 65 | #' geom_polygon(data = ignore.df, aes(x = x, y = y, fill = "1. ignore"))+ 66 | #' geom_point(data = ignore.df, aes(x = x, y = y), color = "black")+ 67 | #' geom_polygon(data = replace.df, aes(x = x + 2, y = y, fill = "2. replace"))+ 68 | #' geom_point(data = replace.df, aes(x = x + 2, y = y), color = "black")+ 69 | #' geom_polygon(data = add.df, aes(x = x + 4, y = y, fill = "3. add"))+ 70 | #' geom_point(data = add.df, aes(x = x + 4, y = y), fill = NA, color = "black")+ 71 | #' scale_fill_manual("", values = c("red", "green", "blue"))+ 72 | #' coord_fixed() 73 | #' # When original.vertices = "ignore", the interpolated vertices contain only the 74 | #' # first vertex from the original polygon, which leads to a different shape. 75 | #' # The vertices are, however, evenly distributed (i.e. the distance between consecutive 76 | #' # vertices is constant). With "replace", the interpolated vertices contain all 77 | #' # of the original vertices, but they are not evenly distributed along 78 | #' # the boundary. The same holds for "add", where the interpolated vertices 79 | #' # contains both the vertices shown in "ignore" and the original vertices. 80 | #' # Note: These differences are very noticeable since num.interp is small. 81 | #' # Usually, we will use more than 18 interpolated vertices. 82 | #' 83 | #' @export 84 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 85 | interpolate_polygon = function(vertex.df, method = "linear") { 86 | check_vertex_df(vertex.df) 87 | n = nrow(vertex.df) 88 | 89 | # Add first vertex to end to simplify computations 90 | vertex.df = vertex.df[c(1:n, 1), ] 91 | 92 | # Vector of distances between vertices 93 | edge.length = sqrt(diff(vertex.df$x)^2 + diff(vertex.df$y)^2) 94 | 95 | # The proportion of each edge relative to the whole perimeter 96 | proportion = edge.length / sum(edge.length) 97 | proportion.cumulative = c(0, cumsum(proportion)) 98 | 99 | if (method == "linear") { 100 | spline.x = stats::approxfun(proportion.cumulative, vertex.df$x) 101 | spline.y = stats::approxfun(proportion.cumulative, vertex.df$y) 102 | } else if (method == "spline") { 103 | spline.x = stats::splinefun(proportion.cumulative, vertex.df$x, method = "periodic") 104 | spline.y = stats::splinefun(proportion.cumulative, vertex.df$y, method = "periodic") 105 | } else { 106 | stop("Invalid value for method, must be either \"linear\" or \"spline\".") 107 | } 108 | 109 | original.time = proportion.cumulative[-(n + 1)] 110 | 111 | interpolation_function = function(time.values, original.vertices = "ignore") { 112 | if (0 %in% time.values && 1 %in% time.values) { 113 | warning("time.values contains both 0 and 1. These correspond to the same ", 114 | "point on the polygon.") 115 | } 116 | if (original.vertices == "replace") { 117 | if (length(time.values) < n) { 118 | stop('When original.vertices == "replace", the length of time.values ', 119 | 'must be equal to or greater than nrow(vertex.df).') 120 | } 121 | index.match = multiple_closest_values(time.values, original.time, num.closest = n) 122 | fixed.time = time.values 123 | fixed.time[index.match[, 1]] = original.time[index.match[, 2]] 124 | 125 | } else if (original.vertices == "add") { 126 | fixed.time = sort(unique(c(time.values, original.time))) 127 | } else if (original.vertices == "ignore") { 128 | fixed.time = time.values 129 | } else { 130 | stop('Invalid values for original.vertices, must be either "replace", "add", ', 131 | 'or "ignore".') 132 | } 133 | 134 | x = spline.x(fixed.time) 135 | y = spline.y(fixed.time) 136 | return(data.frame(x = x, y = y, time = fixed.time)) 137 | } 138 | 139 | return(interpolation_function) 140 | } 141 | -------------------------------------------------------------------------------- /R/deform-polygon.R: -------------------------------------------------------------------------------- 1 | #' Deform polygon 2 | #' 3 | #' This function deforms a polygon by moving each vertex a specified distance. 4 | #' If you want a smooth deformation of a polygon with few vertices (e.g. a square), 5 | #' then \code{\link{interpolate_polygon}} can be used to create an interpolated version 6 | #' of the polygon (see example below). 7 | #' 8 | #' @inheritParams cut_polygons 9 | #' @param deform.distance The distance that each vertex is to be moved. Can 10 | #' either be a single value or a vector containing one value per vertex. 11 | #' If the value is positive, the vertex is moved "outwards", while a negative 12 | #' value moves the vertex "inwards". 13 | #' @param method Must be one of the following values: 14 | #' \itemize{ 15 | #' \item{"bisector", where the vertex is moved along the bisector of the two adjacent edges} 16 | #' \item{"centroid", where the vertex is moved along the line between the vertex 17 | #' and the centroid of the polygon} 18 | #' } 19 | #' 20 | #' @note This function is not robust, and can lead to strange results. While 21 | #' \code{method = "bisector"} generally looks better, it struggles in certain 22 | #' situations (see example #3 below), and should only be used for expanding convex 23 | #' polygons. If the polygon is concave and \code{deform.distance} contains 24 | #' both positive and negative values, \code{method = "centroid"} might be a 25 | #' better choice. 26 | #' 27 | #' @return The same data frame that is given in \code{vertex.df}, the only difference 28 | #' being that the \code{x}- and \code{y}-columns are changed due to the deformation. 29 | #' 30 | #' @examples 31 | #' # Example 1: Deformation of rounded square 32 | #' # Start with a square 33 | #' square.df = data.frame(x = c(-1, 1, 1, -1), y = c(1, 1, -1, -1)) 34 | #' # Round the corners of the square 35 | #' rounded.df = round_polygon_corners(square.df, corner.radius = 0.3) 36 | #' # The rounded polygon has many vertices along the corners, but none along the 37 | #' # straight edges. We need to resample! 38 | #' # Create function that allows us to resample vertices along the boundary of the polygon 39 | #' interpolation.function = interpolate_polygon(rounded.df) 40 | #' # How many vertices should resampled polygon contain? 41 | #' num.vertices = 1000 42 | #' # Create vector with n values evenly spaced between 0 and 1 43 | #' # (0 and 1 correspond to the same point, so the latter is removed) 44 | #' time = seq(0, 1, length.out = num.vertices + 1)[-(num.vertices + 1)] 45 | #' 46 | #' # Polygon that consists of 1000 vertices placed evenly along the boundary of 47 | #' # the rounded polygon 48 | #' resampled.df = interpolation.function(time) 49 | #' 50 | #' # Function that gives the amount of deformation for time between 0 and 1 51 | #' deform.function = function(t, freq) 0.1 + (0.35 - 0.1)*(1 + cos(freq*2*pi*t)) / 2 52 | #' # Vector that gives the amount of deformation for each vertex in resampled.df 53 | #' deform.amount = deform.function(time, 4) 54 | #' 55 | #' # Deformed polygon 56 | #' deform.df = deform_polygon(resampled.df, deform.amount, method = "bisector") 57 | #' 58 | #' library(ggplot2) 59 | #' ggplot()+ 60 | #' geom_polygon(data = square.df, aes(x = x, y = y, color = "1. Original"), fill = NA)+ 61 | #' geom_polygon(data = rounded.df, aes(x = x, y = y, color = "2. Rounded"), fill = NA)+ 62 | #' geom_polygon(data = deform.df, aes(x = x, y = y, color = "3. Deformed"), fill = NA)+ 63 | #' scale_color_manual(values = c("black", "blue", "red"))+ 64 | #' coord_fixed() 65 | #' 66 | #' # Example 2: Comparison of "bisector" and "centroid" methods 67 | #' # Start with a 2 x 10 rectangle 68 | #' rectangle.df = data.frame(x = c(-1, 1, 1, -1), y = c(5, 5, -5, -5)) 69 | #' 70 | #' # Resample polygon, this time without rounding 71 | #' interpolation.function = interpolate_polygon(rectangle.df) 72 | #' resampled.df = interpolation.function(time) 73 | #' 74 | #' # Deformation that oscillates faster than in example 1 75 | #' deform.amount = deform.function(time, 20) 76 | #' 77 | #' # Deformed polygons 78 | #' bisector.df = deform_polygon(resampled.df, deform.amount, method = "bisector") 79 | #' centroid.df = deform_polygon(resampled.df, deform.amount, method = "centroid") 80 | #' 81 | #' # From left to right, the plot shows the original rectangle, and the deformed polygons 82 | #' # from the "bisector" and "centroid" methods, respectively. 83 | #' # The result from "bisector" looks better overall, but struggles at sharp corners, 84 | #' # where the deformed polygon ends up having large gaps between the vertices. 85 | #' # This is avoided by rounding the polygon before deformation. 86 | #' # The deformed polygon from the "centroid" method handles the corners without problem, 87 | #' # but the amount of deformation is not consistent along the vertical sides. 88 | #' ggplot()+ 89 | #' geom_polygon(data = rectangle.df, aes(x = x, y = y), fill = "red")+ 90 | #' geom_polygon(data = bisector.df, aes(x = x + 3, y = y), fill = "red")+ 91 | #' geom_polygon(data = centroid.df, aes(x = x + 6, y = y), fill = "red")+ 92 | #' geom_point(data = bisector.df, aes(x = x + 3, y = y), color = "black", size = 0.5)+ 93 | #' geom_point(data = centroid.df, aes(x = x + 6, y = y), color = "black", size = 0.5)+ 94 | #' coord_fixed() 95 | #' 96 | #' # Example 3: Situation where "bisector" method fails 97 | #' set.seed(123) 98 | #' # Generate random polygon using polar coordinates 99 | #' num.vertices = 10 100 | #' theta = seq(0, 2*pi, length.out = num.vertices + 1)[-(num.vertices + 1)] 101 | #' radius = runif(num.vertices, 0.5, 1) 102 | #' vertex.df = data.frame(x = radius*cos(theta), y = radius*sin(theta)) 103 | #' # How the original polygon looks: 104 | #' ggplot()+ 105 | #' geom_polygon(data = vertex.df, aes(x = x, y = y), fill = NA, color = "black")+ 106 | #' coord_fixed() 107 | #' 108 | #' # Resample polygon before deforming 109 | #' interpolation.function = interpolate_polygon(vertex.df) 110 | #' resampled.df = interpolation.function(time) 111 | #' 112 | #' # Deform function goes from -0.2 to 0.2, and back to -0.2 113 | #' deform.function = function(t) -0.2*cos(2*pi*t) 114 | #' deform.amount = deform.function(time) 115 | #' 116 | #' # Deform resampled polygon 117 | #' deform.df = deform_polygon(resampled.df, deform.amount) 118 | #' 119 | #' # How the deformed polygon looks: 120 | #' ggplot()+ 121 | #' geom_polygon(data = deform.df, aes(x = x, y = y), fill = NA, color = "black")+ 122 | #' coord_fixed() 123 | #' # The combinations convex corner/negative deformation (right side of plot) 124 | #' # and concave corner/positive deformation (left side) leads to a shape that is 125 | #' # self-intersecting. 126 | #' @export 127 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 128 | deform_polygon = function(vertex.df, deform.distance, method = "bisector") { 129 | check_vertex_df(vertex.df) 130 | n = nrow(vertex.df) 131 | 132 | if (!(length(deform.distance) %in% c(1, n))) { 133 | stop("deform.distance must either be a single value or a vector of length ", 134 | "nrow(vertex.df).") 135 | } 136 | 137 | if (method == "bisector") { 138 | unit.deform.vector = compute_deform_direction_bisector(vertex.df) 139 | } else if (method == "centroid") { 140 | unit.deform.vector = compute_deform_direction_centroid(vertex.df) 141 | } else { 142 | stop('method must be either "bisector" or "centroid".') 143 | } 144 | 145 | deform.df = vertex.df 146 | deform.df[, c("x", "y")] = deform.df[, c("x", "y")] + deform.distance * unit.deform.vector 147 | 148 | return(deform.df) 149 | } 150 | 151 | compute_deform_direction_bisector = function(vertex.df) { 152 | n = nrow(vertex.df) 153 | vector.sign = ifelse(is_polygon_ccw(vertex.df), 1, -1) 154 | 155 | vertex.before = vertex.df[c(n, 1:(n-1)), c("x", "y")] 156 | vertex.after = vertex.df[c(2:n, 1), c("x", "y")] 157 | 158 | angle.before = atan2(vertex.before$y - vertex.df$y, vertex.before$x - vertex.df$x) %% (2*pi) 159 | angle.after = atan2(vertex.after$y - vertex.df$y, vertex.after$x - vertex.df$x) %% (2*pi) 160 | angle.internal = angle.after - angle.before 161 | angle.middle = (angle.before + angle.after) / 2 162 | 163 | fix.indices = angle.internal < 0 164 | angle.middle[fix.indices] = angle.middle[fix.indices] + pi 165 | 166 | unit.deform.vector = vector.sign*data.frame(x = cos(angle.middle), y = sin(angle.middle)) 167 | return(unit.deform.vector) 168 | } 169 | 170 | compute_deform_direction_centroid = function(vertex.df) { 171 | centroid = compute_polygon_centroid(vertex.df) 172 | 173 | deform.vector = data.frame( 174 | x = vertex.df$x - centroid$x, 175 | y = vertex.df$y - centroid$y 176 | ) 177 | unit.deform.vector = normalize_vectors(deform.vector) 178 | return(unit.deform.vector) 179 | } 180 | -------------------------------------------------------------------------------- /R/round-polygon-corners.R: -------------------------------------------------------------------------------- 1 | #' Rounds the corners of a polygon 2 | #' 3 | #' Takes in a single polygon and rounds it by replacing the corner vertices with circular arcs. 4 | #' 5 | #' @inheritParams interpolate_polygon 6 | #' @param corner.radius Determines the corner radius of each circular art. Can be one 7 | #' of the following: 8 | #' \itemize{ 9 | #' \item{A single value, which will be used for every circular arc/corner} 10 | #' \item{A numeric vector of length nrow(vertex.df), where each value specifies 11 | #' the corner radius used for the corresponding vertex} 12 | #' \item{"constant", where every corner uses the same, optimal radius. This is the 13 | #' largest possible radius while keeping things "nice and smooth"} 14 | #' \item{"varying", which computes the largest possible corner radius for each vertex} 15 | #' } 16 | #' @param corner.radius.scale A number that each corner radius is multiplied by. 17 | #' Useful when \code{corner.radius} is "constant" or "varying", and you wish to 18 | #' reduce the size of the corner radius values. 19 | #' @param max.vertices.per.corner Controls the number of vertices used in the circular arcs 20 | #' that replace the corners. This depends on how sharp the corner is: the sharper the 21 | #' corner, the more vertices are needed to create a smooth arc. 22 | #' 23 | #' @note If the corner radius values are specified manually, the results will not necessarily 24 | #' look good, and it may take some trial and error. The option "varying" is experimental, 25 | #' and can be bit aggressive. See the example for a comparison between the different 26 | #' options. 27 | #' 28 | #' @return A data frame that contains the x- and y-coordinates of the rounded polygon. 29 | #' 30 | #' @examples 31 | #' # Generate a random polygon based on polar coordinates 32 | #' set.seed(321) 33 | #' n = 20 # Number of vertices 34 | #' theta = rev(seq(0, 2*pi, length.out=n)[-(n + 1)]) 35 | #' radius = runif(n, 10, 20) 36 | #' vertex.df = data.frame(x = radius*cos(theta), y = radius*sin(theta)) 37 | #' #' 38 | #' # Plot original polygon 39 | #' library(ggplot2) 40 | #' ggplot() + geom_polygon(data = vertex.df, aes(x = x, y = y))+coord_fixed() 41 | #' #' 42 | #' # Every corner is rounded with the same specified radius 43 | #' rounded.1 = round_polygon_corners(vertex.df, corner.radius = 1) 44 | #' ggplot() + geom_polygon(data = rounded.1, aes(x = x, y = y)) + coord_fixed() 45 | #' #' 46 | #' # The corners are rounded with individual, specified radius 47 | #' rounded.2 = round_polygon_corners(vertex.df, corner.radius = runif(n, 0, 1.9)) 48 | #' ggplot() + geom_polygon(data = rounded.2, aes(x = x, y = y)) + coord_fixed() 49 | #' #' 50 | #' # Every corner is rounded with the same optimal radius 51 | #' rounded.3 = round_polygon_corners(vertex.df, corner.radius = "constant") 52 | #' ggplot() + geom_polygon(data = rounded.3, aes(x = x, y = y)) + coord_fixed() 53 | #' #' 54 | #' # The corners are rounded with optimal individual radius 55 | #' rounded.4 = round_polygon_corners(vertex.df, corner.radius = "varying") 56 | #' ggplot() + geom_polygon(data = rounded.4, aes(x = x, y = y)) + coord_fixed() 57 | #' 58 | #' # Comparison of different options 59 | #' ggplot()+ 60 | #' geom_polygon(data = rounded.1, aes(x = x, y = y, fill = "1. Constant, manual"))+ 61 | #' geom_polygon(data = rounded.2, aes(x = x + 40, y = y, fill = "2. Varying, manual"))+ 62 | #' geom_polygon(data = rounded.3, aes(x = x, y = y - 40, fill = "3. Constant, optimal"))+ 63 | #' geom_polygon(data = rounded.4, aes(x = x + 40, y = y - 40, fill = "4. Varying, optimal"))+ 64 | #' coord_fixed() 65 | #' 66 | #' # Below we what happens if the corner radius is too large: 67 | #' # The circular arcs are not connected in a smooth way 68 | #' rounded.5 = round_polygon_corners(vertex.df, corner.radius = 4) 69 | #' ggplot() + geom_polygon(data = rounded.5, aes(x = x, y = y)) + coord_fixed() 70 | #' 71 | #' @export 72 | #' @author Mathias Isaksen \email{mathiasleanderi@@gmail.com} 73 | round_polygon_corners = function(vertex.df, corner.radius = "constant", corner.radius.scale = 1, max.vertices.per.corner = 50) { 74 | if ("group" %in% colnames(vertex.df) && length(unique(vertex.df$group)) > 1) { 75 | stop("The group column in vertex.df contains more than one unique value. round_polygon_corners ", 76 | "can only handle one polygon per call.") 77 | } 78 | check_vertex_df(vertex.df) 79 | n = nrow(vertex.df) 80 | is.ccw = is_polygon_ccw(vertex.df) 81 | 82 | vertex.previous = vertex.df[c(n, 1:(n-1)), c("x", "y")] 83 | vertex.next = vertex.df[c(2:n, 1), c("x", "y")] 84 | 85 | edge.lengths = sqrt(rowSums((vertex.df[, c("x", "y")] - vertex.next)^2)) 86 | 87 | angle.previous = atan2(vertex.previous$y - vertex.df$y, vertex.previous$x - vertex.df$x) 88 | angle.next = atan2(vertex.next$y - vertex.df$y, vertex.next$x - vertex.df$x) 89 | 90 | # The interior angle is found by taking the difference between 91 | # the angle between the the current vertex and the previous, 92 | # and the angle between the current vertex and the next 93 | if (is.ccw) { 94 | angle.interior = (angle.previous - angle.next) %% (2*pi) 95 | } else { 96 | angle.interior = (angle.next - angle.previous) %% (2*pi) 97 | } 98 | 99 | # A corner is convex if the interior angle is less than 180 degrees 100 | is.convex = angle.interior < pi 101 | # How much of a circle is needed to represent the corner? 102 | arc.size = ifelse(is.convex, pi - angle.interior, angle.interior - pi) 103 | # The smallest of angle.interior and 2*pi - angle.interior 104 | angle.span = ifelse(is.convex, angle.interior, 2*pi - angle.interior) 105 | # Should the rouding be done inwards (concave) or outwards (convex)? 106 | offset.direction = ifelse(is.convex, -1, 1) 107 | 108 | if (length(corner.radius) == 1 && corner.radius == "varying") { 109 | corner.radius = compute_varying_corner_radius(edge.lengths, angle.span) 110 | } else if (length(corner.radius) == 1 && corner.radius == "constant") { 111 | corner.radius = compute_constant_corner_radius(edge.lengths, angle.span) 112 | } else if (is.numeric(corner.radius) && length(corner.radius) == 1) { 113 | corner.radius = rep(corner.radius, n) 114 | } else if (!(is.numeric(corner.radius) & length(corner.radius) == n)) { 115 | stop("corner.radius must be either \"varying\", \"constant\", a single number or a numeric of length nrow(vertex.df)") 116 | } 117 | corner.radius = corner.radius.scale*corner.radius 118 | # How far should the center of the rounding circle be from the vertex? 119 | offset.amount = corner.radius / sin(angle.span / 2) 120 | # deform.polygon deals with computing the centers of the rounding circles 121 | arc.centers = deform_polygon(vertex.df, offset.amount*offset.direction) 122 | 123 | # The angle between the vertices and the circle centers 124 | vertex.angle = atan2(vertex.df$y - arc.centers$y, vertex.df$x - arc.centers$x) 125 | 126 | # How many vertices should be used in the rounding portion? 127 | vertices.per.corner = ceiling(max.vertices.per.corner * arc.size / pi) 128 | # Using an arbitrary lower limit of 3 129 | vertices.per.corner[vertices.per.corner < 3] = 3 130 | 131 | # Numerics needed for computing rounded polygon 132 | arc.size.total = rep(arc.size, vertices.per.corner) 133 | arc.centers.total = arc.centers[rep(1:n, vertices.per.corner), ] 134 | vertex.angle.total = rep(vertex.angle, vertices.per.corner) 135 | is.convex.total = rep(is.convex, vertices.per.corner) 136 | corner.radius.total = rep(corner.radius, vertices.per.corner) 137 | 138 | total.vertices = sum(vertices.per.corner) 139 | 140 | # Gives the polar coordinate angles for every rounded corner, going from 0 to arc.size 141 | arc.basis = arc.size.total*(0:(total.vertices - 1) - rep(cumsum(c(0, vertices.per.corner))[1:n], vertices.per.corner)) / rep(vertices.per.corner - 1, vertices.per.corner) 142 | # Offsets so that the angles are symmetric around 0 (going from -arc.size / 2 to arc.size / 2) 143 | arc.symmetric = arc.basis - arc.size.total / 2 144 | # Flip direction if polygon is not ccw 145 | arc.symmetric = if(is.ccw) arc.symmetric else -arc.symmetric 146 | # Flip direction of concave corners, as these corners are rounded inwards 147 | arc.symmetric[!is.convex.total] = -arc.symmetric[!is.convex.total] 148 | # Rotate according to position of circle center relative to vertex position 149 | arc.angles = arc.symmetric + vertex.angle.total 150 | 151 | arc.points = corner.radius.total * data.frame(x = cos(arc.angles), y = sin(arc.angles)) 152 | rounded.df = data.frame( 153 | x = arc.centers.total$x + arc.points$x, 154 | y = arc.centers.total$y + arc.points$y 155 | ) 156 | # Remove consecutive vertices in the same location 157 | rounded.df = remove_duplicate_vertices(rounded.df) 158 | 159 | return(rounded.df) 160 | } 161 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # artKIT 5 | 6 | 7 | 8 | ## Overview 9 | 10 | artKIT is an R package that includes a number of functions that are 11 | useful when creating visual/generative art. At the moment, most of the 12 | package deals with polygons/shapes, exposing functions that allows the 13 | user to do operations such as boundary interpolation, corner rounding, 14 | deformation and geometric subdivison/splitting. There are also functions 15 | for rescaling values and creating images based on functions that map 16 | pixel locations to colors. 17 | 18 | The package is well-documented, and every function has both examples of 19 | usage and descriptions of functionality, parameters and return value. 20 | 21 | Vectorization is used throughout the package, leading to efficient 22 | functions. 23 | 24 | artKIT is a work in progress, and will be updated as soon as more 25 | functions are ready. If you discover any issues or bugs, [please let me 26 | know](https://github.com/mathiasisaksen/artkit/issues/new). 27 | 28 | ## Installation 29 | 30 | The package can be installed by using the function `install_github` from 31 | `devtools`: 32 | 33 | ``` r 34 | devtools::install_github("mathiasisaksen/artKIT") 35 | ``` 36 | 37 | ## Summary of content 38 | 39 | We’ll start with the functions that deal with polygons: 40 | 41 | - `interpolate_polygon`: Takes in a polygon and returns a function 42 | that interpolates the boundary of the polygon. This is useful for 43 | - `round_polygon_corners`: Takes in a polygon and corner radius 44 | values, and returns a smoothed version of the polygon where the 45 | corners have been replaced by circular arcs (see the red shape in 46 | the logo) 47 | - `deform_polygon`: Takes in a polygon and the amount of deformation 48 | to apply to each vertex, and returns a deformed version of the 49 | polygon (see the cyan shape in the logo) 50 | - `cut_polygons`: Takes in one or more polygons and the number of 51 | times the polygons are to be subdivided/cut in half, and returns the 52 | cut polygons (see the black and white shape in the logo) 53 | - `transition_between_polygons`: Takes in two polygons and 54 | interpolates/transitions between them 55 | - `generate_random_polygon`: Generates a random polygon using polar 56 | coordinates 57 | - `rotate_polygon`: Takes in one or more polygons, the amount each 58 | polygon is to be rotated and the centers of rotation, and returns 59 | the rotated polygons 60 | - `compute_polygon_area`: Takes in one or more polygons and computes 61 | their areas 62 | - `compute_polygon_centroid`: Takes in one or more polygons and 63 | computes their centroids 64 | - `compute_polygon_perimeter`: Takes in one or more polygons and 65 | computes their perimeters 66 | - `compute_regular_polygons`: Computes one or more regular polygons 67 | with specified centers, circumradiuses and number of edges 68 | - `rounded_rectangle`: Creates a rectangle with rounded corners 69 | - `rounded_regular_polygon`: Creates a regular polygon with rounded 70 | corners 71 | 72 | The remaining functions are the following: 73 | 74 | - `image_from_function`: Takes in a function that maps 2D locations to 75 | colors, and writes a PNG image based on this 76 | - `normalize_vectors`: Takes in one or more vectors and normalizes 77 | their lengths to 1 78 | - `minmax_scaling`: Rescales a vector of numbers to specified lower 79 | and upper bounds 80 | - `uniform_scaling`: Transforms a vector of numbers to be uniformly 81 | distributed on the interval \[0, 1\] 82 | 83 | ## Usage 84 | 85 | The documentation for a given function can be found by writing, for 86 | example, `?round_polygon_corners`. Writing 87 | `example("round_polygon_corners")` lets you interactively run the code 88 | examples in the documentation. 89 | 90 | Here are a couple of demonstrations of how the package can be used. 91 | 92 | ### Example of polygon functions 93 | 94 | In this example we start with a square and end up with a deformed shape 95 | cut into many pieces. 96 | 97 | First, we create a data frame for the square: 98 | 99 | ``` r 100 | # Data frame containing the vertices of the square 101 | vertex.df = data.frame(x = c(1, -1, -1, 1), y = c(1, 1, -1, -1)) 102 | 103 | library(ggplot2) 104 | # Plot showing the square and its vertices 105 | ggplot()+ 106 | geom_polygon(data = vertex.df, aes(x = x, y = y), fill = "red")+ 107 | geom_point(data = vertex.df, aes(x = x, y = y), color = "black", size = 0.5)+ 108 | coord_fixed() 109 | ``` 110 | 111 | ![](man/figures/README-square-1.png) 112 | 113 | The polygon is shown in red, and the vertices are shown as black points. 114 | 115 | Next, we round the corners of the square. 116 | 117 | ``` r 118 | library(artKIT) 119 | rounded.df = round_polygon_corners(vertex.df, corner.radius = 0.2) 120 | print(nrow(rounded.df)) 121 | ## [1] 100 122 | ``` 123 | 124 | This replaces every vertex with a circular arc, and the result is a 125 | smooth shape: 126 | 127 | ``` r 128 | ggplot()+ 129 | geom_polygon(data = rounded.df, aes(x = x, y = y), fill = "red")+ 130 | geom_point(data = rounded.df, aes(x = x, y = y), color = "black", size = 0.5)+ 131 | coord_fixed() 132 | ``` 133 | 134 | ![](man/figures/README-round_plot-1.png) 135 | 136 | While rounding the corners of the polygon before deformation is not 137 | required, it usually leads to better results (see example 2 under 138 | `?deform_polygon` for deformation without rounding). 139 | 140 | The rounded polygon contains 100 vertices, and these are all located 141 | along the corners. Before we deform it, we should resample points more 142 | evenly along the boundary: 143 | 144 | ``` r 145 | # Resample 1000 points along the boundary 146 | num.vertices = 1000 147 | # Get num.vertices + 1 evenly placed values between 0 and 1, remove the latter since 148 | # 0 and 1 correspond to the first vertex 149 | interp.time = seq(0, 1, length.out = num.vertices + 1)[-(num.vertices + 1)] 150 | # Create interpolation function and evaluate it at interp.time 151 | resample.df = interpolate_polygon(rounded.df)(interp.time) 152 | 153 | ggplot()+ 154 | geom_polygon(data = resample.df, aes(x = x, y = y), fill = "red")+ 155 | geom_point(data = resample.df, aes(x = x, y = y), color = "black", size = 0.5)+ 156 | coord_fixed() 157 | ``` 158 | 159 | ![](man/figures/README-interpolate-1.png) 160 | 161 | That looks better! `interpolate_polygon` returns a function that 162 | interpolates the boundary of the polygon. The function takes in values 163 | between 0 and 1, and maps these to locations on the boundary. When the 164 | input is 0, the position of the first vertex is returned. 0.5 gives the 165 | position halfway around the perimeter, and 1 closes the loop by again 166 | giving the position of the first vertex. 167 | 168 | Next, we deform the resampled polygon. This is done using the function 169 | `deform_polygon`, which moves every vertex a specified distance. A 170 | positive distance moves the vertex “outwards”, while a negative distance 171 | moves it “inwards”. We define `deform.function`, which gives every 172 | vertex a deform distance between 0.1 and 0.3. 173 | 174 | ``` r 175 | deform.function = function(t) 0.1 + (0.3 - 0.1)*(1 + cos(6*2*pi*t))/2 176 | deform.distance = deform.function(interp.time) 177 | 178 | deform.df = deform_polygon(resample.df, deform.distance) 179 | 180 | ggplot()+ 181 | geom_polygon(data = deform.df, aes(x = x, y = y), fill = "red")+ 182 | geom_point(data = deform.df, aes(x = x, y = y), color = "black", size = 0.5)+ 183 | coord_fixed() 184 | ``` 185 | 186 | ![](man/figures/README-deform-1.png) 187 | 188 | It’s important to use a periodic function when computing the deform 189 | distance. Otherwise, there will be a gap between the first and the last 190 | vertex. 191 | 192 | Next, we’ll cut the shape into many small pieces using the function 193 | `cut_polygons`. At each iteration, it cuts every piece along a randomly 194 | generated line, and the total number of polygons will, at minimum, be 195 | doubled. 196 | 197 | ``` r 198 | cut.df = cut_polygons(deform.df, number.of.iterations = 12, seed = 123) 199 | 200 | ggplot()+ 201 | geom_polygon(data = cut.df, aes(x = x, y = y, group = group), 202 | fill = NA, color = "black", size = 0.2)+ 203 | coord_fixed() 204 | ``` 205 | 206 | ![](man/figures/README-cut-1.png) 207 | 208 | ## Example of `image_from_function` 209 | 210 | In this simple example, we use a 2D noise function from the package 211 | [GRFics](https://github.com/mathiasisaksen/GRFics) combined with 212 | `image_from_function` to create a PNG image. 213 | 214 | We start out by setting up the GRF object: 215 | 216 | ``` r 217 | library(GRFics) 218 | grf.object = generate.grf.object( 219 | x.lower = -1.4, x.upper = 1.4, y.lower = -1, y.upper = 1, 220 | resolution.x = 490, resolution.y = 350, 221 | range.parameter = 0.3, direction.parameter = pi/4, 222 | strength.parameter = 3, 223 | function.names = "color.value" 224 | ) 225 | ``` 226 | 227 | We use the rectangle (-1.4, 1.4) x (-1, 1) as our plot area, leading to 228 | an aspect ratio of 14:10. The GRF object contains one noise function 229 | named `color.value`. 230 | 231 | Next, we set up the color function that will be passed to 232 | `image_from_function`. This function is expected to take in the 233 | positions of the pixels and return one color per pixel. The vector 234 | `colors` contains the colors we use in the image, and `color.ramp` 235 | interpolates between these colors. `color.function` first computes a 236 | value between 0 and 1 for each pixel, using the noise function 237 | `color.value`. Then, these values are plugged into `color.ramp`, which 238 | returns the colors in matrix format. Finally, we divide by 255 to get 239 | RGB components between 0 and 1. 240 | 241 | ``` r 242 | colors = c("#cb0560", "#ffffff") 243 | color.ramp = colorRamp(colors) 244 | color.function = function(locations) { 245 | color.value = evaluate.grf(locations, grf.object, function.name = "color.value", 246 | rescale.method = "uniform") 247 | rgb.matrix = color.ramp(color.value) / 255 248 | return(rgb.matrix) 249 | } 250 | ``` 251 | 252 | In `image_from_function`, the size of the output image is specified with 253 | `width` and `height`. The parameter `bounds` determines the bounds of 254 | the rectangular grid that the pixels are placed in. 255 | 256 | ``` r 257 | image_from_function( 258 | filename = "image.png", 259 | color.function = color.function, 260 | bounds = c(-1.4, 1.4, -1, 1), 261 | width = 1050, height = 750, 262 | ) 263 | ``` 264 | 265 | ![](man/figures/README-image.png) 266 | -------------------------------------------------------------------------------- /src/polygoncutting.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /* 12 | This is a single-file version of the code, for use with Rcpp. 13 | See https://github.com/mathiasisaksen/polygon-cutting for better structured code. 14 | */ 15 | 16 | // [[Rcpp::plugins("cpp11")]] 17 | 18 | // polygon.h 19 | 20 | const double EPS = std::numeric_limits::epsilon(); 21 | const double PI_DOUBLE = 3.14159265358979323846; 22 | const double INF_DOUBLE = std::numeric_limits::infinity(); 23 | 24 | bool isZero(double a); 25 | double dmod(double number, double modulus); 26 | 27 | class Vector; 28 | 29 | struct Point 30 | { 31 | double x, y; 32 | Point() : x(0), y(0) { ; } 33 | Point(double x, double y) : x(x), y(y) { ; } 34 | Point(Vector v); 35 | }; 36 | 37 | class Vector 38 | { 39 | private: 40 | double x, y; 41 | 42 | public: 43 | Vector() : x(0), y(0) { ; } 44 | Vector(double x, double y) : x(x), y(y) { ; } 45 | Vector(Point p) : x(p.x), y(p.y) { ; } 46 | Vector operator=(Point p); 47 | friend Vector operator*(double a, const Vector v); 48 | friend Vector operator+(Vector v1, Vector v2); 49 | friend Vector operator-(Vector v1, Vector v2); 50 | const double getX() { return x; } 51 | const double getY() { return y; } 52 | const double getLength() { return std::sqrt(std::pow(x, 2) + std::pow(y, 2)); } 53 | friend std::ostream &operator<<(std::ostream &out, const Vector &v); 54 | }; 55 | 56 | double crossProduct(Vector v1, Vector v2); 57 | double dotProduct(Vector v1, Vector v2); 58 | 59 | class Line 60 | { 61 | private: 62 | Vector start, direction; 63 | 64 | public: 65 | Line() : start(0, 0), direction(0, 0) { ; } 66 | Line(Point p1, Point p2); 67 | Line(Vector start, Vector direction) : start(start), direction(direction) { ; } 68 | Line(Point start, Vector direction) : start(Vector(start)), direction(direction) { ; } 69 | Vector getStart() { return start; } 70 | Vector getDirection() { return direction; } 71 | }; 72 | 73 | double distancePointToLine(Point &p, Line &l); 74 | std::ostream &operator<<(std::ostream &out, Line &l); 75 | 76 | class Intersection 77 | { 78 | private: 79 | Point position; 80 | double time1; 81 | double time2; 82 | int index; 83 | 84 | public: 85 | Intersection(Point position, double time1, double time2) : position(position), time1(time1), time2(time2) { ; } 86 | Point getPosition() { return position; } 87 | double getTime1() const { return time1; } 88 | double getTime2() const { return time2; } 89 | void setIndex(int index) { this->index = index; } 90 | int getIndex() const { return index; } 91 | }; 92 | 93 | std::ostream &operator<<(std::ostream &out, Intersection &i); 94 | bool operator<(const Intersection &lhs, const Intersection &rhs); 95 | Intersection computeLineIntersection(Line l1, Line l2); 96 | 97 | struct AABB 98 | { 99 | double xMin, xMax, yMin, yMax; 100 | AABB() : xMin(INF_DOUBLE), xMax(-INF_DOUBLE), yMin(INF_DOUBLE), yMax(-INF_DOUBLE) { ; } 101 | AABB(double xMin, double xMax, double yMin, double yMax) : xMin(xMin), xMax(xMax), yMin(yMin), yMax(yMax) { ; } 102 | }; 103 | 104 | class Polygon 105 | { 106 | private: 107 | std::vector points; 108 | Point centroid; 109 | double area; 110 | AABB aabb; 111 | void computeAreaAndCentroid(); 112 | void computeAABB(); 113 | 114 | public: 115 | Polygon(std::vector points); 116 | std::vector getPoints() { return points; } 117 | Point getCentroid() { return centroid; } 118 | double getArea() { return area; } 119 | int getNumberOfPoints() { return points.size(); } 120 | bool isPolygonCCW(); 121 | bool isPointInPolygon(Point point); 122 | bool isConvex(); 123 | Point sampleUniform(); 124 | }; 125 | 126 | std::ostream &operator<<(std::ostream &out, Polygon &polygon); 127 | 128 | template 129 | std::ostream &operator<<(std::ostream &out, std::vector &vector); 130 | 131 | template 132 | std::vector getSubVector(std::vector vector, int start, int end); 133 | 134 | std::vector splitPolygon(Polygon polygon, Point cutPoint, double cutDirection); 135 | std::vector splitPolygonRecursive(Polygon polygon, Point cutPoint, double cutDirection); 136 | std::vector splitPolygonHelper(Polygon polygon, Line cutLine, double minTime = -INF_DOUBLE); 137 | 138 | // polygon.cpp 139 | 140 | bool isZero(double a) 141 | { 142 | return std::abs(a) < EPS; 143 | } 144 | 145 | double dmod(double number, double modulus) 146 | { 147 | return number - std::floor(number / modulus) * modulus; 148 | } 149 | 150 | template 151 | std::ostream &operator<<(std::ostream &out, std::vector &vector) 152 | { 153 | for (int i = 0; i < vector.size(); i++) 154 | { 155 | out << i << ": " << vector[i] << std::endl; 156 | } 157 | return out; 158 | } 159 | 160 | template 161 | std::vector getSubVector(std::vector vector, int start, int end) 162 | { 163 | int n = vector.size(); 164 | if (start >= end) 165 | { 166 | end += n; 167 | } 168 | std::vector subVector; 169 | subVector.reserve(end - start + 1); 170 | 171 | for (int i = start; i <= end; i++) 172 | { 173 | subVector.push_back(vector[i % n]); 174 | } 175 | return (subVector); 176 | } 177 | 178 | Point::Point(Vector v) 179 | { 180 | this->x = v.getX(); 181 | this->y = v.getY(); 182 | } 183 | 184 | Vector Vector::operator=(Point p) 185 | { 186 | return Vector(p.x, p.y); 187 | } 188 | 189 | Vector operator*(double a, Vector v) 190 | { 191 | return Vector(a * v.getX(), a * v.getY()); 192 | } 193 | 194 | Vector operator+(Vector v1, Vector v2) 195 | { 196 | return Vector(v1.getX() + v2.getX(), v1.getY() + v2.getY()); 197 | } 198 | 199 | Vector operator-(Vector v1, Vector v2) 200 | { 201 | return Vector(v1.getX() - v2.getX(), v1.getY() - v2.getY()); 202 | } 203 | 204 | std::ostream &operator<<(std::ostream &out, const Vector &v) 205 | { 206 | return out << "(" << v.x << ", " << v.y << ")"; 207 | } 208 | 209 | double crossProduct(Vector v1, Vector v2) 210 | { 211 | return v1.getX() * v2.getY() - v1.getY() * v2.getX(); 212 | } 213 | 214 | double dotProduct(Vector v1, Vector v2) 215 | { 216 | return v1.getX() * v2.getX() + v1.getY() * v2.getY(); 217 | } 218 | 219 | Line::Line(Point start, Point end) 220 | { 221 | this->start = Vector(start); 222 | this->direction = Vector(end) - Vector(start); 223 | } 224 | 225 | double distancePointToLine(Point &q, Line &l) 226 | { 227 | Vector k = Vector(q) - l.getStart(); 228 | Vector r = l.getDirection(); 229 | return std::sqrt(std::pow(k.getLength(), 2) - std::pow(dotProduct(k, r) / r.getLength(), 2)); 230 | } 231 | 232 | Intersection computeLineIntersection(Line l1, Line l2) 233 | { 234 | // Approach from https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect 235 | Vector p = l1.getStart(); 236 | Vector r = l1.getDirection(); 237 | Vector q = l2.getStart(); 238 | Vector s = l2.getDirection(); 239 | double cp = crossProduct(r, s); 240 | double time1 = crossProduct(q - p, s) / cp; 241 | double time2 = crossProduct(q - p, r) / cp; 242 | Point position = p + time1 * r; 243 | Intersection result(position, time1, time2); 244 | return result; 245 | } 246 | 247 | std::ostream &operator<<(std::ostream &out, Line &l) 248 | { 249 | return out << l.getStart() << " + " << l.getDirection() << "*t"; 250 | } 251 | 252 | std::ostream &operator<<(std::ostream &out, Intersection &i) 253 | { 254 | return out << "Position: " << i.getPosition() << " Time 1: " << i.getTime1() << " Time 2: " << i.getTime2() << " Index: " << i.getIndex() << std::endl; 255 | } 256 | 257 | bool operator<(const Intersection &lhs, const Intersection &rhs) 258 | { 259 | return lhs.getTime1() < rhs.getTime1(); 260 | } 261 | 262 | void Polygon::computeAreaAndCentroid() 263 | { 264 | double signedArea = 0; 265 | double cx = 0; 266 | double cy = 0; 267 | for (int i = 0, j = 1; i < points.size(); i++, j = (i + 1) % points.size()) 268 | { 269 | signedArea += 0.5 * (points[i].x * points[j].y - points[j].x * points[i].y); 270 | cx += (points[i].x + points[j].x) * (points[i].x * points[j].y - points[j].x * points[i].y); 271 | cy += (points[i].y + points[j].y) * (points[i].x * points[j].y - points[j].x * points[i].y); 272 | } 273 | this->area = std::abs(signedArea); 274 | this->centroid = Point(cx / (6 * signedArea), cy / (6 * signedArea)); 275 | } 276 | 277 | void Polygon::computeAABB() 278 | { 279 | for (int i = 0; i < points.size(); i++) 280 | { 281 | double x = points[i].x; 282 | double y = points[i].y; 283 | aabb.xMin = std::min(x, aabb.xMin); 284 | aabb.xMax = std::max(x, aabb.xMax); 285 | aabb.yMin = std::min(y, aabb.yMin); 286 | aabb.yMax = std::max(y, aabb.yMax); 287 | } 288 | } 289 | 290 | Polygon::Polygon(std::vector points) 291 | { 292 | this->points = points; 293 | computeAreaAndCentroid(); 294 | computeAABB(); 295 | } 296 | 297 | bool Polygon::isPointInPolygon(Point point) 298 | { 299 | double x = point.x; 300 | double y = point.y; 301 | 302 | bool isInPolygon = false; 303 | int numVertices = points.size(); 304 | for (int i = 0, j = numVertices - 1; i < numVertices; j = i++) 305 | { 306 | Point a = points[i]; 307 | Point b = points[j]; 308 | 309 | if (a.y > b.y) 310 | { 311 | a = points[j]; 312 | b = points[i]; 313 | } 314 | 315 | if (((y > a.y) != (y > b.y)) && ((x - a.x) * (b.y - a.y) < (y - a.y) * (b.x - a.x))) 316 | { 317 | isInPolygon = !isInPolygon; 318 | } 319 | } 320 | 321 | return isInPolygon; 322 | } 323 | 324 | bool Polygon::isPolygonCCW() 325 | { 326 | int n = points.size(); 327 | // Find vertex with lowest x-coordinate (and highest y-coordinate if there are ties) 328 | Point minXPoint = points[0]; 329 | int minXIndex = 0; 330 | for (int i = 1; i < n; i++) 331 | { 332 | Point curPoint = points[i]; 333 | 334 | if ((curPoint.x < minXPoint.x) || 335 | ((curPoint.x == minXPoint.x) && (curPoint.y > minXPoint.y))) 336 | { 337 | minXPoint = curPoint; 338 | minXIndex = i; 339 | } 340 | } 341 | int prevIndex = minXIndex == 0 ? n - 1 : minXIndex - 1; 342 | int nextIndex = minXIndex == n - 1 ? 0 : minXIndex + 1; 343 | 344 | Point prevPoint = points[prevIndex]; 345 | Point nextPoint = points[nextIndex]; 346 | 347 | double det = (minXPoint.x - prevPoint.x) * (nextPoint.y - prevPoint.y) - 348 | (nextPoint.x - prevPoint.x) * (minXPoint.y - prevPoint.y); 349 | return det > 0; 350 | } 351 | 352 | bool Polygon::isConvex() 353 | { 354 | // A polygon is convex if every internal angle is less than or equal to pi 355 | int n = points.size(); 356 | bool isCCW = isPolygonCCW(); 357 | int sign = isCCW ? 1 : -1; 358 | for (int i = 0; i < n; i++) 359 | { 360 | Point curPoint = points[i]; 361 | Point prevPoint = points[i == 0 ? n - 1 : i - 1]; 362 | Point nextPoint = points[i == n - 1 ? 0 : i + 1]; 363 | 364 | double firstAngle = std::atan2(prevPoint.y - curPoint.y, 365 | prevPoint.x - curPoint.x); 366 | double secondAngle = std::atan2(nextPoint.y - curPoint.y, 367 | nextPoint.x - curPoint.x); 368 | double internalAngle = dmod(sign * (firstAngle - secondAngle), 2 * PI_DOUBLE); 369 | if (internalAngle > PI_DOUBLE) 370 | { 371 | return false; 372 | } 373 | } 374 | return true; 375 | } 376 | 377 | Point Polygon::sampleUniform() 378 | { 379 | Point sample; 380 | do 381 | { 382 | sample.x = Rcpp::runif(1, aabb.xMin, aabb.xMax)[0]; 383 | sample.y = Rcpp::runif(1, aabb.yMin, aabb.yMax)[0]; 384 | } while (!isPointInPolygon(sample)); 385 | return sample; 386 | } 387 | 388 | std::ostream &operator<<(std::ostream &out, Polygon &polygon) 389 | { 390 | out << "Centroid: " << polygon.getCentroid() << std::endl; 391 | out << "Area: " << polygon.getArea() << std::endl; 392 | std::vector points = polygon.getPoints(); 393 | for (int i = 0; i < points.size(); i++) 394 | { 395 | out << points[i] << std::endl; 396 | } 397 | return out; 398 | } 399 | 400 | std::vector splitPolygon(Polygon polygon, Point cutPoint, double cutDirection) 401 | { 402 | Vector cutVector(std::cos(cutDirection), std::sin(cutDirection)); 403 | Line cutLine(cutPoint, cutVector); 404 | 405 | std::vector points = polygon.getPoints(); 406 | std::vector pointsLeft, pointsRight; 407 | 408 | std::vector posIndices, negIndices; 409 | std::vector posPoints, negPoints; 410 | int curPosIndex(0), curNegIndex(0); 411 | for (int i = 0; i < points.size(); i++) 412 | { 413 | Point p = points[i]; 414 | double cp = crossProduct(Vector(p) - Vector(cutPoint), cutVector); 415 | if (cp > 0) 416 | { 417 | if (posIndices.size() == 0) 418 | { 419 | posIndices.push_back(i); 420 | posPoints.push_back(p); 421 | } 422 | else 423 | { 424 | if (posIndices[curPosIndex - 1] != i - 1) 425 | { 426 | curPosIndex = 0; 427 | } 428 | posIndices.insert(posIndices.begin() + curPosIndex, i); 429 | posPoints.insert(posPoints.begin() + curPosIndex, p); 430 | } 431 | curPosIndex++; 432 | } 433 | else 434 | { 435 | if (negIndices.size() == 0) 436 | { 437 | negIndices.push_back(i); 438 | negPoints.push_back(p); 439 | } 440 | else 441 | { 442 | if (negIndices[curNegIndex - 1] != i - 1) 443 | { 444 | curNegIndex = 0; 445 | } 446 | negIndices.insert(negIndices.begin() + curNegIndex, i); 447 | negPoints.insert(negPoints.begin() + curNegIndex, p); 448 | } 449 | curNegIndex++; 450 | } 451 | } 452 | Line line1 = Line(posPoints[0], negPoints[negPoints.size() - 1]); 453 | Intersection intersection1 = computeLineIntersection(cutLine, line1); 454 | Intersection intersection2 = computeLineIntersection(cutLine, Line(negPoints[0], posPoints[posPoints.size() - 1])); 455 | posPoints.insert(posPoints.begin(), intersection1.getPosition()); 456 | posPoints.push_back(intersection2.getPosition()); 457 | negPoints.insert(negPoints.begin(), intersection2.getPosition()); 458 | negPoints.push_back(intersection1.getPosition()); 459 | std::vector result; 460 | result.push_back(Polygon(posPoints)); 461 | result.push_back(Polygon(negPoints)); 462 | return result; 463 | } 464 | 465 | std::vector splitPolygonRecursive(Polygon polygon, Point cutPoint, double cutDirection) 466 | { 467 | Vector cutVector(std::cos(cutDirection), std::sin(cutDirection)); 468 | Line cutLine(cutPoint, cutVector); 469 | std::vector cutPolygons = splitPolygonHelper(polygon, cutLine); 470 | 471 | return cutPolygons; 472 | } 473 | 474 | std::vector splitPolygonHelper(Polygon polygon, Line cutLine, double minTime) 475 | { 476 | int n = polygon.getNumberOfPoints(); 477 | std::vector intersections; 478 | std::vector points = polygon.getPoints(); 479 | 480 | for (int i = 0, j = 1; i < n; i++, j = (i + 1) % n) 481 | { 482 | Line edge(points[i], points[j]); 483 | Intersection intersection = computeLineIntersection(cutLine, edge); 484 | intersection.setIndex(i); 485 | 486 | // An intersection is valid if it intersects the edge (0 <= time2 <= 1), 487 | // and happens later than any earlier intersections 488 | if (intersection.getTime2() > 0 && intersection.getTime2() < 1 && 489 | intersection.getTime1() > minTime) 490 | { 491 | intersections.push_back(intersection); 492 | } 493 | } 494 | if (intersections.size() < 2) 495 | { 496 | std::vector result; 497 | result.push_back(polygon); 498 | return result; 499 | } 500 | // Sort intersections by the order they occur along the line 501 | std::sort(intersections.begin(), intersections.end()); 502 | Intersection intersection1 = intersections[0]; 503 | Intersection intersection2 = intersections[1]; 504 | double nextMinTime = intersection2.getTime1(); 505 | 506 | int index1 = intersection1.getIndex(); 507 | int index2 = intersection2.getIndex(); 508 | 509 | // Pick out one point too many at start and end, and replace with intersection points 510 | std::vector points1 = getSubVector(polygon.getPoints(), index1, (index2 + 1) % n); 511 | points1[0] = intersection1.getPosition(); 512 | points1[points1.size() - 1] = intersection2.getPosition(); 513 | 514 | std::vector points2 = getSubVector(polygon.getPoints(), index2, (index1 + 1) % n); 515 | points2[0] = intersection2.getPosition(); 516 | points2[points2.size() - 1] = intersection1.getPosition(); 517 | 518 | Polygon polygon1(points1); 519 | Polygon polygon2(points2); 520 | std::vector cutPolygons1 = splitPolygonHelper(polygon1, cutLine, nextMinTime); 521 | std::vector cutPolygons2 = splitPolygonHelper(polygon2, cutLine, nextMinTime); 522 | cutPolygons1.insert(cutPolygons1.end(), cutPolygons2.begin(), cutPolygons2.end()); 523 | return cutPolygons1; 524 | } 525 | 526 | // cut.h 527 | 528 | class Cut 529 | { 530 | private: 531 | std::vector polygons; 532 | 533 | public: 534 | Cut() : polygons() { ; } 535 | Cut(std::vector polygons) : polygons(polygons) { ; } 536 | Cut(Polygon polygon); 537 | int getNumberOfPolygons() { return polygons.size(); } 538 | int getTotalNumberOfPoints(); 539 | Polygon getPolygon(int i) { return polygons[i]; } 540 | void addPolygon(Polygon polygon); 541 | void addPolygons(std::vector &polygons); 542 | void replacePolygon(Polygon newPolygon, int replaceIndex); 543 | }; 544 | 545 | Cut cutEveryPolygon(Cut &cut, bool useCentroid = true); 546 | void cutSinglePolygon(Cut &cut, int polygonIndex); 547 | 548 | // cut.cpp 549 | 550 | Cut::Cut(Polygon polygon) 551 | { 552 | polygons.push_back(polygon); 553 | } 554 | 555 | int Cut::getTotalNumberOfPoints() 556 | { 557 | int total = 0; 558 | for (int i = 0; i < this->getNumberOfPolygons(); i++) 559 | { 560 | total += this->getPolygon(i).getNumberOfPoints(); 561 | } 562 | return total; 563 | } 564 | 565 | void Cut::addPolygon(Polygon polygon) 566 | { 567 | this->polygons.push_back(polygon); 568 | } 569 | 570 | void Cut::addPolygons(std::vector &newPolygons) 571 | { 572 | for (int i = 0; i < newPolygons.size(); i++) 573 | { 574 | this->polygons.push_back(newPolygons[i]); 575 | } 576 | } 577 | 578 | void Cut::replacePolygon(Polygon newPolygon, int replaceIndex) 579 | { 580 | polygons[replaceIndex] = newPolygon; 581 | } 582 | 583 | Cut cutEveryPolygon(Cut &cut, bool useCentroid) 584 | { 585 | Cut newCut; 586 | for (int i = 0; i < cut.getNumberOfPolygons(); i++) 587 | { 588 | Polygon currentPolygon = cut.getPolygon(i); 589 | Point cutPoint; 590 | if (useCentroid) 591 | { 592 | cutPoint = currentPolygon.getCentroid(); 593 | } 594 | else 595 | { 596 | cutPoint = currentPolygon.sampleUniform(); 597 | } 598 | double direction = Rcpp::runif(1, 0, PI_DOUBLE)[0]; 599 | std::vector polygonSplit; 600 | if (currentPolygon.isConvex()) 601 | { 602 | polygonSplit = splitPolygon(currentPolygon, cutPoint, direction); 603 | } 604 | else 605 | { 606 | polygonSplit = splitPolygonRecursive(currentPolygon, cutPoint, direction); 607 | } 608 | 609 | newCut.addPolygons(polygonSplit); 610 | } 611 | return newCut; 612 | } 613 | 614 | Cut createCut(Rcpp::NumericVector x, Rcpp::NumericVector y, Rcpp::NumericVector group) 615 | { 616 | int n = x.length(); 617 | std::vector points; 618 | std::vector polygons; 619 | int prevGroup = group[0]; 620 | for (int i = 0; i < n; i++) 621 | { 622 | if (group[i] != prevGroup) 623 | { 624 | polygons.push_back(Polygon(points)); 625 | points.clear(); 626 | } 627 | points.push_back(Point(x[i], y[i])); 628 | prevGroup = group[i]; 629 | } 630 | polygons.push_back(Polygon(points)); 631 | Cut output = Cut(polygons); 632 | return output; 633 | } 634 | 635 | // [[Rcpp::export]] 636 | 637 | Rcpp::DataFrame cutEveryPolygonRcpp(Rcpp::NumericVector x, Rcpp::NumericVector y, 638 | Rcpp::NumericVector group, int numberOfIterations = 1, bool useCentroid = true) 639 | { 640 | Cut cut = createCut(x, y, group); 641 | for (int k = 0; k < numberOfIterations; k++) 642 | { 643 | cut = cutEveryPolygon(cut, useCentroid); 644 | } 645 | 646 | int nRows = cut.getTotalNumberOfPoints(); 647 | std::vector xVector, yVector; 648 | std::vector groupVector; 649 | xVector.reserve(nRows); 650 | yVector.reserve(nRows); 651 | groupVector.reserve(nRows); 652 | 653 | for (int i = 0; i < cut.getNumberOfPolygons(); i++) 654 | { 655 | Polygon polygon = cut.getPolygon(i); 656 | std::vector points = polygon.getPoints(); 657 | for (int j = 0; j < points.size(); j++) 658 | { 659 | xVector.push_back(points[j].x); 660 | yVector.push_back(points[j].y); 661 | groupVector.push_back(i + 1); 662 | } 663 | } 664 | Rcpp::DataFrame out = Rcpp::DataFrame::create(Rcpp::Named("x") = xVector, 665 | Rcpp::Named("y") = yVector, 666 | Rcpp::Named("group") = groupVector); 667 | return out; 668 | } 669 | 670 | // [[Rcpp::export]] 671 | 672 | Rcpp::DataFrame cutEveryPolygonReturnAllRcpp(Rcpp::NumericVector x, Rcpp::NumericVector y, Rcpp::NumericVector group, 673 | int numberOfIterations = 1, bool useCentroid = true) 674 | { 675 | Cut cut = createCut(x, y, group); 676 | int nRows = cut.getTotalNumberOfPoints(); 677 | 678 | std::vector cuts; 679 | cuts.reserve(numberOfIterations + 1); 680 | cuts.push_back(cut); 681 | 682 | for (int k = 0; k < numberOfIterations; k++) 683 | { 684 | cut = cutEveryPolygon(cut, useCentroid); 685 | nRows += cut.getTotalNumberOfPoints(); 686 | cuts.push_back(cut); 687 | } 688 | 689 | std::vector xVector, yVector; 690 | std::vector groupVector, iterationVector; 691 | for (int k = 0; k < cuts.size(); k++) 692 | { 693 | Cut currentCut = cuts[k]; 694 | for (int i = 0; i < currentCut.getNumberOfPolygons(); i++) 695 | { 696 | Polygon polygon = currentCut.getPolygon(i); 697 | std::vector points = polygon.getPoints(); 698 | for (int j = 0; j < points.size(); j++) 699 | { 700 | xVector.push_back(points[j].x); 701 | yVector.push_back(points[j].y); 702 | groupVector.push_back(i + 1); 703 | iterationVector.push_back(k); 704 | } 705 | } 706 | } 707 | 708 | Rcpp::DataFrame out = Rcpp::DataFrame::create(Rcpp::Named("x") = xVector, 709 | Rcpp::Named("y") = yVector, 710 | Rcpp::Named("group") = groupVector, 711 | Rcpp::Named("iteration") = iterationVector); 712 | return out; 713 | } 714 | --------------------------------------------------------------------------------