Neon Ghosts with ggplot2

Neon Ghosts with ggplot2

Table of Content

Make your own glowing, wiggly ghosts in R with a dash of randomness and a lot of neon.

Ever wanted to make a haunted house in R? Here’s a quick and fun way to conjure up a whole gang of neon ghosts using ggplot2. Each ghost is a little different — some tall, some stout, all a bit wobbly—thanks to a shape generator that randomizes their size and wiggle.

The plotting function layers up neon glows for the outline, eyes, and mouth, then adds a faint fill and solid black features for that classic ghostly look. Try running it a few times to see what kind of spectral friends you get!

library(ggplot2)
library(scales)

#' Ghost plotting function
#' @param ghost_df Data frame with x and y coordinates of the ghost shape
#' @param ghost_color Color of the ghost (default is neon green)
#' @return ggplot object of the ghost
plot_simple_ghost <- function(ghost_df, ghost_color = "#39FF14") {
  x_center <- mean(range(ghost_df$x))
  y_top <- max(ghost_df$y)
  y_bottom <- min(ghost_df$y)
  ghost_height <- y_top - y_bottom
  ghost_width <- diff(range(ghost_df$x))
  
  # Eyes
  eye_y <- y_bottom + 0.7 * ghost_height
  eye_x <- x_center + c(-0.06, 0.06) * ghost_width
  eyes <- data.frame(x = eye_x, y = rep(eye_y, 2))
  
  # Mouth: several waves 
  mouth_width <- ghost_width * 0.4
  mouth_x <- seq(x_center - 0.3 * mouth_width, x_center + 0.3 * mouth_width, length.out = 80)
  mouth_y <- y_bottom + 0.55 * ghost_height + sin(seq(0, 7 * pi, length.out = 80)) * 0.04 * ghost_height
  mouth <- data.frame(x = mouth_x, y = mouth_y)
  
  # 8 neon glow layers
  glow_layers <- data.frame(
    linewidth = c(12, 10, 8, 6, 4.5, 3, 2, 1),
    alpha = c(0.04, 0.06, 0.09, 0.13, 0.18, 0.25, 0.35, 0.5)
  )
  
  p <- ggplot() +
    geom_polygon(
      data = ghost_df, aes(x, y),
      fill = ghost_color, alpha = 0.15, color = NA
    )
  
  # Glow layers for outline, eyes, and mouth
  for(i in seq_len(nrow(glow_layers))) {
    p <- p +
      geom_polygon(
        data = ghost_df, aes(x, y),
        fill = NA, color = scales::alpha(ghost_color, glow_layers$alpha[i]),
        linewidth = glow_layers$linewidth[i]
      ) +
      geom_point(
        data = eyes, aes(x, y),
        color = scales::alpha(ghost_color, glow_layers$alpha[i]),
        size = glow_layers$linewidth[i] * 1.1
      ) +
      geom_path(
        data = mouth, aes(x, y),
        color = scales::alpha(ghost_color, glow_layers$alpha[i]),
        linewidth = glow_layers$linewidth[i]
      )
  }
  
  # Solid black mouth and eyes
  p <- p +
    geom_path(
      data = mouth, aes(x, y),
      color = "black", linewidth = 1.2
    ) +
    geom_point(
       data = eyes, aes(x, y),
       color = "black",
       size = 1.2
    ) 

  # Fix aspect ratio and theme
  p + 
   coord_fixed(
       xlim = c(-4, 4), 
       ylim = c(-3, 8), 
       expand = TRUE
   ) +
   theme_void() +
   theme(
      plot.background = element_rect(fill = "black", color = NA),
      panel.background = element_rect(fill = "black", color = NA)
    )
}

#' Generate a random ghost shape
#' @param seed Optional seed for reproducibility
#' @return Data frame with x and y coordinates of the ghost shape
ghost_shape <- function(seed = NULL) {
  if (!is.null(seed)) set.seed(seed)
  width <- sample(seq(0.5, 3, length.out = 6), 1)  
  height <- sample(1:6,1) 
  x <- seq(-1, 1, length.out = 100) * width
  amp <- runif(1, 0.15, 0.28)
  freq <- sample(3:7, 1)
  phase <- runif(1, 0, 2*pi)
  y_top <- sqrt(1 - (x/width)^2) * height + runif(1, -0.03, 0.03)
  y_bottom <- -0.5 * height + sin(seq(0, freq * pi, length.out = 100) + phase) * amp * height
  data.frame(
    x = c(x, rev(x)),
    y = c(y_top, rev(y_bottom))
  )
}

ghost_shape() |> 
       plot_simple_ghost(ghost_color = "#39FF14")
ghost_shape() |> 
       plot_simple_ghost(ghost_color = "#FF6EC7")
ghost_shape() |>
       plot_simple_ghost(ghost_color = "#1E90FF")
  • Oct 01, 2025

    Mapply: When You Need to Iterate Over Multiple Inputs

    Learn when and how to use mapply for applying functions with multiple varying arguments. This guide shows practical examples of processing data when you need to pair multiple inputs element-wise.

    Read
    r
    apply
  • Sep 02, 2025

    Hugo modules vs. Git submodules: manage your website more easily

    Explore the dynamics of Hugo modules and Git submodules in managing site dependencies efficiently. I go through their features, advantages, and practical applications in web development, centering on my experience with Hugo and Git.

    Read
    git
    hugo
    submodules
    web-development
    r
  • Aug 01, 2025

    Automate LinkedIn Carousels with R and Gemini

    Learn how to automate LinkedIn carousel creation with R and the free Gemini API. This post details a complete workflow, from connecting to the API and summarizing a blog post into structured points, to generating a custom Quarto document ready for manual review and posting.

    Read
    r
    api
  • Jul 01, 2025

    Decoding OAuth2 M2M with httr2: Client Setup & API Testing

    This post details the process of setting up an OAuth2 Machine-to-Machine (M2M) client using the httr2 package in R. It covers client creation, handling authentication flows, and comprehensively testing API calls using the vcr package for request recording and testthat for mocking functions to ensure robust and reliable package development.

    Read
    r
    api
  • Jun 02, 2025

    Unmasking Long Covid: PCA & Clustering Analysis of Symptom Syndromes

    This blog post delves into the my personal Long Covid data, collected via the Visible app, using Principal Component Analysis (PCA). I detail four key clusters: Menstruation, Exertion, Emotional, and Neurological and then compare these clusters to the principal components derived from PCA. The analysis aims to provide a validated, data-driven framework for exploring my symptoms during this condition, using clear visualisations to showcase the results.

    Read
    r
    health-data
    longcovid