Animated Pulse Flow - xelsed.ai

This sketch paints a nighttime sky filled with softly twinkling stars, then layers three semi-transparent curtains of noise-driven color on top to simulate the shifting glow of an aurora borealis. The green, blue, and purple bands ripple independently using Perlin noise, creating an organic, ever-changing light show.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the night sky color — The background() color is applied every frame - swapping its RGB values changes the whole mood of the scene, for example toward a deep purple twilight.
  2. Make the aurora reach higher — auroraAmplitude controls how far up the green core layer can stretch - dividing height by a smaller number increases the maximum reach.
  3. Fill the sky with more stars — numStars controls how many stars setup() generates - a much higher count creates a dense, glittering starfield behind the aurora.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates the northern lights: a dark starry sky rendered with hundreds of twinkling points, topped by three overlapping curtains of color that ripple and drift like real aurora borealis. It relies almost entirely on Perlin noise (the noise() function) to generate organic, non-repeating motion, along with map() to translate that noise into height and transparency values, and alpha blending to make the light bands glow instead of looking like solid shapes.

The code has three functions: setup() builds an array of star objects with randomized properties, draw() runs every frame to redraw the sky and animate the aurora using three nested for-loops (one per color layer), and windowResized() rebuilds the stars whenever the browser window changes size. Studying it teaches how a single noise function, sampled at different scales and offsets, can drive multiple independent-looking animated effects at once.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas that fills the browser window and generates 300 star objects, each with a random position, size, brightness, and a unique 'twinkleOffset' number used later to desynchronize their twinkling.
  2. Every frame, draw() first paints a dark blue background, then loops through every star and uses noise(star.twinkleOffset + frameCount * 0.01) to smoothly vary its brightness over time, so each star fades in and out at its own pace instead of flickering randomly.
  3. Next, draw() computes shared aurora settings: a base y-position near the bottom of the screen, a maximum height the aurora can reach, and a noiseOffset that advances every frame so the whole pattern drifts sideways over time.
  4. Three separate for-loops then draw the aurora as vertical lines - one for a detailed green core, one for a wider blue glow, and one for the widest, faintest purple glow - each sampling noise() at a different x-scale and time-offset so the three layers move independently and never look identical.
  5. For each vertical line, the noise value at that x-position is mapped to both a y-height (how far up the line reaches) and an alpha transparency value (how visible that segment is), which is what makes the aurora look like it shimmers and undulates rather than just moving up and down.
  6. If the browser window is resized, windowResized() resizes the canvas and throws away the old stars, generating a brand new set positioned correctly for the new canvas dimensions.

🎓 Concepts You'll Learn

Perlin noisemap() for value remappingalpha transparency blendingfor loops for procedural drawingobject arraysresponsive canvas with windowResizedlayered compositing

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It's the right place to build data structures - like this array of star objects - that draw() will read and animate on every subsequent frame.

🔬 This loop fills the sky with 300 stars. What happens visually if numStars is changed to 50? To 1500?

  for (let i = 0; i < numStars; i++) {
    stars.push({
      x: random(width),
      y: random(height),
      size: random(1, 4),
      brightness: random(100, 255),
      twinkleOffset: random(1000) // Unique offset for each star's twinkle
    });
  }
function setup() {
  createCanvas(windowWidth, windowHeight);
  // Initialize stars with random positions, sizes, brightness, and twinkle offsets
  for (let i = 0; i < numStars; i++) {
    stars.push({
      x: random(width),
      y: random(height),
      size: random(1, 4),
      brightness: random(100, 255),
      twinkleOffset: random(1000) // Unique offset for each star's twinkle
    });
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Star Generation Loop for (let i = 0; i < numStars; i++) {

Creates numStars star objects with randomized position, size, brightness, and twinkle timing, storing each one in the stars array

createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window, using p5's built-in windowWidth and windowHeight variables
for (let i = 0; i < numStars; i++) {
Repeats the code inside the loop numStars (300) times to build up the full array of stars
x: random(width),
Picks a random horizontal position anywhere across the canvas width
y: random(height),
Picks a random vertical position anywhere across the canvas height
size: random(1, 4),
Gives each star a random diameter between 1 and 4 pixels so they aren't all identical
brightness: random(100, 255),
Sets a base brightness value that draw() will later vary up and down for the twinkle effect
twinkleOffset: random(1000) // Unique offset for each star's twinkle
Gives each star its own starting point along the noise curve, so all 300 stars twinkle out of sync with each other instead of flashing in unison

draw()

draw() runs continuously at the sketch's frame rate. Here it demonstrates a powerful pattern: sampling the same noise() function with different scales and offsets to produce several independent-looking animated layers from one noise algorithm, then using map() twice per layer - once for shape, once for transparency - to turn a single number into two visual properties at once.

🔬 This loop draws the green core one 2-pixel-wide line at a time. What happens to performance and smoothness if you change 'x += 2' to 'x += 10'? What if you change it to 'x += 1'?

  for (let x = 0; x <= width; x += 2) { // Draw lines every 2 pixels for detail
    let noiseVal = noise(x * noiseScale, noiseOffset);
    // Map noise value to determine the aurora's height at this x-position
    let y = auroraBaseY - map(noiseVal, 0, 1, 0, auroraAmplitude);
    // Vary opacity based on noise value for a shimmering effect
    stroke(greenColor, map(noiseVal, 0, 1, 100, 200));
    // Draw a vertical line from the base to the calculated aurora height
    line(x, auroraBaseY, x, y);
  }

🔬 The star's brightness oscillates using noise sampled at 'frameCount * 0.01'. What happens if you change 0.01 to 0.1? Do the stars twinkle faster or slower?

  let twinkle = noise(star.twinkleOffset + frameCount * 0.01);
    // Map noise value to vary brightness between 50% and 150% of original
    let currentBrightness = map(twinkle, 0, 1, star.brightness * 0.5, star.brightness * 1.5);
function draw() {
  background(0, 0, 50); // Dark blue background for the night sky

  // Draw twinkling stars
  for (let star of stars) {
    // Use Perlin noise for organic twinkling
    let twinkle = noise(star.twinkleOffset + frameCount * 0.01);
    // Map noise value to vary brightness between 50% and 150% of original
    let currentBrightness = map(twinkle, 0, 1, star.brightness * 0.5, star.brightness * 1.5);
    
    fill(255, currentBrightness); // White stars with varying brightness
    noStroke();
    circle(star.x, star.y, star.size); // Draw each star as a small circle
  }

  // --- Aurora borealis simulation ---
  let auroraBaseY = height / 1.5; // The bottom y-coordinate where the aurora starts
  let auroraAmplitude = height / 3; // The maximum upward reach of the aurora from its base
  let noiseScale = 0.003; // Controls the "zoom" or detail level of the Perlin noise
  let noiseOffset = frameCount * 0.005; // Moves the noise over time for animation

  // Define the core colors of the aurora
  let greenColor = color(0, 255, 100);
  let blueColor = color(0, 100, 255);
  let purpleColor = color(150, 0, 255);

  noFill(); // Aurora will be drawn with strokes only

  // Layer 1: Main green curtain (most opaque and detailed)
  strokeWeight(2); // Thin strokes for the core
  for (let x = 0; x <= width; x += 2) { // Draw lines every 2 pixels for detail
    let noiseVal = noise(x * noiseScale, noiseOffset);
    // Map noise value to determine the aurora's height at this x-position
    let y = auroraBaseY - map(noiseVal, 0, 1, 0, auroraAmplitude);
    // Vary opacity based on noise value for a shimmering effect
    stroke(greenColor, map(noiseVal, 0, 1, 100, 200));
    // Draw a vertical line from the base to the calculated aurora height
    line(x, auroraBaseY, x, y);
  }

  // Layer 2: Blue outer glow (less opaque, slightly wider)
  strokeWeight(3); // Slightly thicker strokes for a softer glow
  for (let x = 0; x <= width; x += 4) { // Draw lines every 4 pixels for a softer look
    // Use slightly different noise parameters for a distinct, wider glow
    let noiseVal = noise(x * noiseScale * 0.8, noiseOffset * 0.8 + 100);
    let y = auroraBaseY - map(noiseVal, 0, 1, 0, auroraAmplitude * 1.2); // Wider amplitude
    stroke(blueColor, map(noiseVal, 0, 1, 30, 80)); // Less opaque blue glow
    line(x, auroraBaseY, x, y);
  }

  // Layer 3: Purple fainter glow (least opaque, widest)
  strokeWeight(4); // Thicker strokes for a very soft, diffuse glow
  for (let x = 0; x <= width; x += 6) { // Draw lines every 6 pixels for the most diffuse look
    // Use even more different noise parameters for the outermost, faintest layer
    let noiseVal = noise(x * noiseScale * 0.6, noiseOffset * 0.6 + 200);
    let y = auroraBaseY - map(noiseVal, 0, 1, 0, auroraAmplitude * 1.5); // Widest amplitude
    stroke(purpleColor, map(noiseVal, 0, 1, 10, 40)); // Very faint purple glow
    line(x, auroraBaseY, x, y);
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Star Twinkle Loop for (let star of stars) {

Redraws every star each frame with a brightness that smoothly oscillates using noise, creating the twinkling effect

for-loop Layer 1: Green Core for (let x = 0; x <= width; x += 2) { // Draw lines every 2 pixels for detail

Draws the most detailed, most opaque aurora band using a fine sampling step and the base noise scale

for-loop Layer 2: Blue Glow for (let x = 0; x <= width; x += 4) { // Draw lines every 4 pixels for a softer look

Draws a wider, softer, more transparent glow using a different noise offset so it moves independently of the green layer

for-loop Layer 3: Purple Glow for (let x = 0; x <= width; x += 6) { // Draw lines every 6 pixels for the most diffuse look

Draws the widest, faintest glow layer, completing the illusion of depth in the aurora

background(0, 0, 50); // Dark blue background for the night sky
Repaints the entire canvas with a dark navy color every frame, erasing the previous frame so nothing smears
let twinkle = noise(star.twinkleOffset + frameCount * 0.01);
Samples Perlin noise at a point that slowly moves forward over time (frameCount * 0.01), offset uniquely per star so each one twinkles on its own schedule
let currentBrightness = map(twinkle, 0, 1, star.brightness * 0.5, star.brightness * 1.5);
Converts the noise value (always between 0 and 1) into a brightness between half and one-and-a-half times the star's base brightness
circle(star.x, star.y, star.size); // Draw each star as a small circle
Draws the star as a filled circle at its stored position and size
let auroraBaseY = height / 1.5; // The bottom y-coordinate where the aurora starts
Defines the fixed y-coordinate the aurora bands hang down from, positioned two-thirds of the way down the screen
let noiseOffset = frameCount * 0.005; // Moves the noise over time for animation
Advances a little further along the noise field every frame, which is what makes the aurora appear to drift and ripple rather than stay frozen
noFill(); // Aurora will be drawn with strokes only
Turns off fill so the upcoming line() calls only draw strokes, not filled shapes
for (let x = 0; x <= width; x += 2) { // Draw lines every 2 pixels for detail
Steps across the canvas width, drawing one vertical line every 2 pixels to build up the aurora shape from many thin strokes
let noiseVal = noise(x * noiseScale, noiseOffset);
Samples 2D Perlin noise using the x-position (scaled down) and the animated time offset, producing a smoothly varying value across space and time
let y = auroraBaseY - map(noiseVal, 0, 1, 0, auroraAmplitude);
Converts the noise value into a height above the base, so higher noise values make the aurora reach further up the screen at that x-position
stroke(greenColor, map(noiseVal, 0, 1, 100, 200));
Sets the stroke color to green with an alpha (transparency) that also depends on the noise value, creating a shimmering opacity variation
line(x, auroraBaseY, x, y); // Draw a vertical line from the base to the calculated aurora height
Draws a single vertical line from the base up to the noise-determined height - repeating this across all x-values builds the wavy aurora silhouette

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window changes size. It's essential for responsive sketches - without it, the canvas would stay a fixed size or stretch awkwardly instead of adapting to the viewport.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Regenerate stars to fill the new canvas size
  stars = [];
  for (let i = 0; i < numStars; i++) {
    stars.push({
      x: random(width),
      y: random(height),
      size: random(1, 4),
      brightness: random(100, 255),
      twinkleOffset: random(1000)
    });
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Star Regeneration Loop for (let i = 0; i < numStars; i++) {

Rebuilds the entire stars array with fresh random positions matching the new canvas dimensions

resizeCanvas(windowWidth, windowHeight);
Changes the canvas dimensions to match the browser window's new size, called automatically by p5 whenever the window is resized
stars = [];
Empties the existing stars array so old star positions (which may no longer fit the new canvas) are discarded
for (let i = 0; i < numStars; i++) {
Repeats the same star-creation logic from setup() to refill the array with stars positioned correctly for the new canvas size

📦 Key Variables

stars array

Holds one object per star, each storing its x/y position, size, base brightness, and a unique twinkleOffset used to desynchronize the twinkling animation

let stars = [];
numStars number

Controls how many star objects are generated and drawn each frame - directly determines the density of the starfield

let numStars = 300;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw()

The color() calls for greenColor, blueColor, and purpleColor, along with all the aurora constants (auroraBaseY, auroraAmplitude, noiseScale), are recreated every single frame even though their values never change.

💡 Move these declarations outside draw() into the global scope (or set them once in setup()) so they are computed once instead of 60 times per second.

STYLE setup() and windowResized()

The star-generation loop is duplicated verbatim in both setup() and windowResized(), making it easy for the two copies to drift out of sync if one is edited later.

💡 Extract the loop into a helper function like initStars() and call it from both setup() and windowResized().

PERFORMANCE draw() aurora layers

Three nested loops each iterate across the full canvas width (width/2 + width/4 + width/6 line() calls per frame), which can get expensive on very wide screens or slower devices.

💡 Consider increasing the step size on larger canvases dynamically, or drawing the aurora onto an offscreen buffer with createGraphics() and only updating it every few frames.

FEATURE draw()

The aurora currently has no interactivity - its shape and color are entirely automatic and never respond to the user.

💡 Map mouseX or mouseY to noiseScale, noiseOffset speed, or the color values so moving the mouse changes the aurora's shape, speed, or hue in real time.

🔄 Code Flow

Code flow showing setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> star-init-loop[Star Generation Loop] star-init-loop --> star-twinkle-loop[Star Twinkle Loop] star-twinkle-loop --> layer1-green[Layer 1: Green Core] layer1-green --> layer2-blue[Layer 2: Blue Glow] layer2-blue --> layer3-purple[Layer 3: Purple Glow] draw --> windowresized[windowResized] windowresized --> resize-star-loop[Star Regeneration Loop] resize-star-loop --> star-init-loop click setup href "#fn-setup" click draw href "#fn-draw" click star-init-loop href "#sub-star-init-loop" click star-twinkle-loop href "#sub-star-twinkle-loop" click layer1-green href "#sub-layer1-green" click layer2-blue href "#sub-layer2-blue" click layer3-purple href "#sub-layer3-purple" click resize-star-loop href "#sub-resize-star-loop"

❓ Frequently Asked Questions

What visual effects does the Animated Pulse Flow sketch produce?

The sketch creates a stunning simulation of the aurora borealis, complemented by twinkling stars against a dark blue night sky.

Is there any user interaction available in the Animated Pulse Flow sketch?

The sketch does not include user interaction; it is a visual animation that runs continuously without input.

What creative coding techniques are showcased in the Animated Pulse Flow sketch?

This sketch demonstrates the use of Perlin noise for organic animations and dynamic brightness variations, creating a lifelike representation of the aurora and twinkling stars.

Preview

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