Neon Pulse Rings - xelsed.ai

This sketch creates a mesmerizing tunnel effect using concentric neon-colored rings that continuously expand outward from the center of the screen and fade away. Each ring is a class instance that tracks its own size and transparency, and additive blending makes overlapping rings glow brighter, simulating a neon light effect.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the pulse — Lowering frameInterval spawns new rings much more often, creating a denser, faster tunnel effect.
  2. Make rings race outward — Increasing ringSpeed makes each ring expand much faster, giving the tunnel a rushing, hyperdrive feel.
  3. Swap in a new neon color — Replacing pink with a different color changes the entire palette's mood.
  4. Thicker glowing rings — Increasing strokeWeight makes each ring's outline bolder, intensifying the neon-tube look.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch generates an endless stream of glowing rings that pulse outward from the center of the screen in cyan, magenta, and pink, fading as they travel toward the edges. The mesmerizing tunnel effect comes from three key p5.js techniques working together: an ES6 class to manage each ring's own state, blendMode(ADD) to make overlapping colors glow brighter like real neon light, and the map() function to smoothly fade transparency as each ring grows.

The code is organized around a Ring class with update(), show(), and isDead() methods, plus the standard setup() and draw() functions that spawn new rings on a timer and manage an array of active ones. By studying it you'll learn how to model animated objects as classes, how to use frameCount for timing instead of manual counters, how additive blending creates glow effects, and how to safely remove finished items from an array while looping over it.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, builds the neonColors array with cyan, magenta, and pink, and calculates maxRadius based on the window's size
  2. Every frame, draw() paints a black background and switches to ADD blend mode so overlapping ring colors brighten each other for a glowing effect
  3. Every 15 frames (frameInterval), a brand new Ring object is created at the center of the screen with a random color from the neon palette and pushed into the rings array
  4. The draw loop walks backward through the rings array, calling update() on each ring to grow its radius and fade its alpha based on how long it has existed
  5. Each ring's show() method draws it as a hollow, thick-stroked circle using its current radius and alpha, so it appears as a glowing outline
  6. Once a ring's alpha reaches 0 or its radius passes maxRadius, isDead() returns true and the ring is removed from the array with splice(), keeping the animation running efficiently forever

🎓 Concepts You'll Learn

ES6 classesArray management with spliceAdditive blend modeAlpha fading with map()frameCount-based timingObject-oriented animation

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the ideal place to build lookup arrays like neonColors and calculate values that depend on canvas size, like maxRadius.

function setup() {
  createCanvas(windowWidth, windowHeight); // Create a canvas that fills the window
  pixelDensity(1); // Set pixel density to 1 for consistent appearance across devices

  // Add the neon colors to the palette
  neonColors.push(color(0, 255, 255)); // Cyan
  neonColors.push(color(255, 0, 255)); // Magenta
  neonColors.push(color(255, 105, 180)); // Pink

  // Determine the maximum radius a ring can reach, scaling with the window size
  maxRadius = max(width, height) * 0.7;
}
Line-by-line explanation (6 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window.
pixelDensity(1);
Forces the canvas to render at 1 pixel per pixel, avoiding blurry or overly heavy rendering on high-density screens.
neonColors.push(color(0, 255, 255)); // Cyan
Creates a p5 color object for cyan and adds it to the neonColors array.
neonColors.push(color(255, 0, 255)); // Magenta
Adds magenta to the color palette array.
neonColors.push(color(255, 105, 180)); // Pink
Adds pink to the color palette array.
maxRadius = max(width, height) * 0.7;
Sets how large a ring can grow before disappearing, based on whichever of width or height is bigger, scaled down to 70%.

draw()

draw() runs continuously about 60 times per second. Here it handles three responsibilities each frame: clearing the screen, spawning new rings on a timer, and updating/drawing/removing every ring currently alive - a very common pattern for particle-style animations.

🔬 This block spawns one new ring every frameInterval frames. What happens visually if you push two rings here instead of one, each with a slightly different initialRadius?

  if (frameCount % frameInterval === 0) {
    let initialRadius = 5; // Rings start small
    // Pick a random color from the neon palette
    let colorIndex = floor(random(neonColors.length));
    rings.push(new Ring(width / 2, height / 2, initialRadius, neonColors[colorIndex]));
  }
function draw() {
  background(0); // Dark background

  // Set the blend mode to ADD for a glowing effect.
  // When colors are added together, they become brighter, simulating neon glow.
  blendMode(ADD); // https://p5js.org/reference/#/p5/blendMode

  // Periodically create a new ring
  if (frameCount % frameInterval === 0) {
    let initialRadius = 5; // Rings start small
    // Pick a random color from the neon palette
    let colorIndex = floor(random(neonColors.length));
    rings.push(new Ring(width / 2, height / 2, initialRadius, neonColors[colorIndex]));
  }

  // Iterate through the rings array from back to front
  // This allows safe removal of rings without affecting the loop index
  for (let i = rings.length - 1; i >= 0; i--) {
    rings[i].update(); // Update the ring's size and fade
    rings[i].show(); // Draw the ring
    if (rings[i].isDead()) {
      rings.splice(i, 1); // Remove the ring if it's faded out or too large
      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
    }
  }

  // Reset the blend mode to default (BLEND) for any other drawing operations
  // This is good practice if you plan to draw other elements later in draw()
  blendMode(BLEND); // https://p5js.org/reference/#/p5/blendMode
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Ring Spawn Timer if (frameCount % frameInterval === 0) {

Uses the modulo operator on frameCount to create a new ring every 15 frames without needing a separate counter variable

for-loop Update, Draw, and Remove Rings for (let i = rings.length - 1; i >= 0; i--) {

Loops backward through the rings array so items can be safely removed with splice() mid-loop without skipping elements

background(0);
Fills the canvas with solid black every frame, erasing the previous frame's drawing.
blendMode(ADD);
Switches to additive color blending, so overlapping shapes add their brightness together, producing a glowing neon look.
if (frameCount % frameInterval === 0) {
Checks if the current frame number divides evenly by 15, which happens every 15 frames - this is how a new ring is spawned on a regular interval.
let colorIndex = floor(random(neonColors.length));
Picks a random whole-number index into the neonColors array so each new ring gets a random neon color.
rings.push(new Ring(width / 2, height / 2, initialRadius, neonColors[colorIndex]));
Creates a new Ring object centered on the canvas and adds it to the rings array to be animated.
for (let i = rings.length - 1; i >= 0; i--) {
Loops through the rings array backward (from the last element to the first) which is the safe way to remove items during iteration.
rings[i].update();
Calls the ring's own update method to recalculate its current radius and fading alpha.
rings[i].show();
Calls the ring's show method to actually draw it on screen.
if (rings[i].isDead()) {
Checks whether this ring has fully faded or grown too large.
rings.splice(i, 1);
Removes exactly one element at index i from the rings array, deleting the finished ring.
blendMode(BLEND);
Restores the default blend mode so any future drawing isn't affected by the additive glow effect.

Ring (class)

This class demonstrates a powerful pattern: giving each animated object its own data (position, birth time, color) and its own methods (update, show, isDead) so main draw() loop stays simple and just delegates work to each instance.

class Ring {
  constructor(x, y, initialRadius, color) {
    this.x = x; // X-coordinate of the ring's center
    this.y = y; // Y-coordinate of the ring's center
    this.initialRadius = initialRadius; // Radius when the ring is created
    this.color = color; // Color of the ring
    this.birthFrame = frameCount; // Frame count when this ring was born
    this.currentRadius = initialRadius; // Current radius of the ring
    this.alpha = 255; // Current alpha (transparency) of the ring
  }

  update() {
    this.currentRadius = this.initialRadius + (frameCount - this.birthFrame) * ringSpeed;
    this.alpha = map(this.currentRadius, this.initialRadius, maxRadius, 255, 0);
    this.alpha = max(0, this.alpha);
  }

  show() {
    this.color.setAlpha(this.alpha);
    noFill();
    stroke(this.color);
    strokeWeight(3);
    circle(this.x, this.y, this.currentRadius * 2);
  }

  isDead() {
    return this.alpha <= 0 || this.currentRadius > maxRadius;
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Radius Growth Formula this.currentRadius = this.initialRadius + (frameCount - this.birthFrame) * ringSpeed;

Calculates how much a ring has grown by multiplying the number of frames since birth by the speed constant

calculation Alpha Fade Mapping this.alpha = map(this.currentRadius, this.initialRadius, maxRadius, 255, 0);

Converts the ring's current size into a transparency value, so small rings are opaque and large rings are nearly invisible

this.birthFrame = frameCount;
Records the exact frame number when this ring was created, so its age can be calculated later without a separate timer variable.
this.currentRadius = this.initialRadius + (frameCount - this.birthFrame) * ringSpeed;
Calculates elapsed frames since birth and multiplies by ringSpeed to determine how far the ring has expanded.
this.alpha = map(this.currentRadius, this.initialRadius, maxRadius, 255, 0);
Uses map() to convert the ring's current radius into an opacity value: fully opaque (255) when small, fading to 0 as it approaches maxRadius.
this.alpha = max(0, this.alpha);
Prevents alpha from ever going negative, which could happen if the radius overshoots maxRadius before isDead() catches it.
this.color.setAlpha(this.alpha);
Updates the ring's stored p5.Color object with the newly calculated transparency.
noFill();
Tells p5 not to fill the circle's interior, so only the outline is drawn.
stroke(this.color);
Sets the outline color to this ring's color (including its current alpha).
strokeWeight(3);
Makes the ring's outline 3 pixels thick, giving it a bold neon-tube look.
circle(this.x, this.y, this.currentRadius * 2);
Draws the ring as a circle; circle() takes a diameter, so the radius is doubled.
return this.alpha <= 0 || this.currentRadius > maxRadius;
Returns true when the ring is fully faded OR has expanded past the maximum allowed radius, signaling it should be removed.

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window is resized, letting you keep responsive sketches in sync with the viewport.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // https://p5js.org/reference/#/p5/resizeCanvas
  // Recalculate maxRadius based on new window size
  maxRadius = max(width, height) * 0.7;
  // Clear all existing rings for simplicity, so they restart from the new center
  rings = [];
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Automatically called by p5 when the browser window changes size; this resizes the canvas to match the new window dimensions.
maxRadius = max(width, height) * 0.7;
Recalculates the maximum ring radius since the canvas dimensions have changed.
rings = [];
Empties the array of active rings so the animation restarts cleanly from the new center point instead of showing rings positioned for the old canvas size.

📦 Key Variables

rings array

Stores all currently active Ring objects; grows as new rings spawn and shrinks as old ones are removed.

let rings = [];
neonColors array

Holds the palette of p5.Color objects (cyan, magenta, pink) that rings randomly pick from.

const neonColors = [];
frameInterval number

Number of frames to wait between spawning each new ring, controlling how densely packed the rings appear.

const frameInterval = 15;
ringSpeed number

How many pixels a ring's radius grows per frame, controlling how fast rings expand outward.

const ringSpeed = 1.5;
maxRadius number

The radius at which a ring is considered fully expanded and removed; calculated from canvas size.

let maxRadius;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE Ring.show()

this.color.setAlpha(this.alpha) mutates the shared color object stored in neonColors indirectly through reference, since colors pushed into rings share the same p5.Color instance passed from the palette array (each ring stores a reference, not a copy).

💡 Create a new color per ring using color(red(this.color), green(this.color), blue(this.color)) in the constructor, or use color.levels.slice() to clone it, to avoid any risk of one ring's alpha change affecting another that shares the same object reference.

STYLE draw()

The ring creation logic (initialRadius, colorIndex, and new Ring()) is inlined directly in draw(), mixing spawn logic with the main loop's responsibilities.

💡 Extract this into a helper function like spawnRing() to keep draw() focused purely on orchestrating updates, which makes the code easier to read and test.

FEATURE Ring class

All rings currently spawn from the exact center and use the same speed and interval, making the animation fairly uniform over time.

💡 Add slight randomness to ringSpeed or initialRadius per ring (e.g. ringSpeed * random(0.8, 1.2)) for more organic, less mechanical-looking pulses.

BUG windowResized()

Clearing rings = [] on every resize can cause a jarring visual pop if the user resizes the window frequently or drags a window edge.

💡 Consider repositioning existing rings to the new center instead of deleting them, e.g. update each ring's this.x and this.y to width/2 and height/2 during resize.

🔄 Code Flow

Code flow showing setup, draw, ring, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> spawnring[spawn-ring] draw --> ringloop[ring-loop] click setup href "#fn-setup" click draw href "#fn-draw" click spawnring href "#sub-spawn-ring" click ringloop href "#sub-ring-loop" spawnring -->|Every 15 frames| draw ringloop -->|For each ring| radiuscalc[radius-calc] ringloop -->|For each ring| alphafade[alpha-fade] ringloop -->|Update, Draw, Remove| draw click radiuscalc href "#sub-radius-calc" click alphafade href "#sub-alpha-fade" radiuscalc -->|Calculate growth| ringloop alphafade -->|Map size to transparency| ringloop windowresized[windowresized] -->|Adjust canvas size| draw click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual effect does the Neon Pulse Rings sketch create?

The sketch produces a mesmerizing display of concentric rings that pulse outward from the center, glowing in vibrant neon colors like cyan, magenta, and pink against a dark background.

Is there any interaction possible with the Neon Pulse Rings sketch?

The sketch is primarily a visual experience and does not include interactive elements; users can simply enjoy the hypnotic animation.

What creative coding concepts are showcased in the Neon Pulse Rings sketch?

This sketch demonstrates concepts such as object-oriented programming with the Ring class, animation through frame counting, and dynamic color manipulation based on radius and alpha transparency.

Preview

Neon Pulse Rings - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Neon Pulse Rings - xelsed.ai - Code flow showing setup, draw, ring, windowresized
Code Flow Diagram