Starfield Attempt - xelsed.ai

This sketch simulates a classic 'warp speed' starfield, where hundreds of stars rush outward from the center of the screen toward the viewer. Each star accelerates as it approaches, and a semi-transparent background creates streaking light trails behind every star, mimicking the visual of a spaceship jumping to lightspeed.

🧪 Try This!

Experiment with the code by making these changes:

  1. Supercharge the star count — More stars make the warp field feel much denser and more chaotic - try pushing the count way up.
  2. Stretch the light trails — Lowering the background alpha means each frame fades more slowly, leaving longer, streakier trails behind every star.
  3. Recolor the star field — Swap white and blue for a warm palette to completely change the mood of the effect.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a classic sci-fi 'warp speed' effect: hundreds of stars burst outward from the center of the screen, accelerating and growing as they approach the viewer, leaving soft glowing trails behind them. It's built almost entirely around a custom Star class, p5.Vector for movement direction, a pseudo-3D depth value (z) faked with map() and perspective math, and a semi-transparent background() call that creates motion-blur trails instead of a hard screen clear.

The code is organized into three top-level p5.js functions - setup(), draw(), and windowResized() - plus a Star class with reset(), update(), and show() methods that handle a single star's full life cycle. By studying it you'll learn how object-oriented particle systems work in p5.js, how a fake 'z' coordinate can simulate 3D depth on a flat 2D canvas, and how transparency-based backgrounds produce trailing/glow effects without any actual trail-drawing code.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, turns off shape outlines with noStroke(), and fills the stars array with 400 new Star objects, each starting at a random depth via its constructor calling reset().
  2. Every frame, draw() paints a nearly-black rectangle over the whole canvas using background(0, 50) - because the alpha is only 50 instead of 255, the previous frame isn't fully erased, so old star positions fade out gradually instead of vanishing instantly, producing light trails.
  3. Still inside draw(), a for loop calls update() and show() on every star: update() moves the star closer to the viewer by shrinking its z value and speeds it up slightly using starAcceleration, simulating acceleration as it 'passes' the camera.
  4. show() converts each star's abstract z depth into an on-screen size and position using map() and vector math, so stars near z=0 appear positioned far from center at the perspective-projected screen coordinates, while three extra glow ellipses are layered behind the main star for a soft halo.
  5. When a star's z value drops to 0 or below (it has 'passed' the camera), update() calls reset() again, giving it a brand new random depth, direction, speed, size, and color so it can begin its journey outward once more - creating an endless, self-renewing stream of stars.
  6. windowResized() rebuilds the entire stars array whenever the browser window changes size, keeping the effect centered and correctly scaled to the new canvas dimensions.

🎓 Concepts You'll Learn

Object-oriented particle systems (classes)Pseudo-3D perspective projection on a 2D canvasp5.Vector for directionAlpha transparency for motion trailsThe animation loop (draw())map() and constrain() for value scaling

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It's the right place to configure the canvas and create your initial set of objects - here, a full array of Star instances that draw() will animate forever after.

function setup() {
  // Create a standard 2D canvas that fills the window
  createCanvas(windowWidth, windowHeight);
  
  // Disable stroke for all shapes (stars and glow)
  noStroke(); 
  
  // Initialize the array of stars
  for (let i = 0; i < numStars; i++) {
    stars.push(new Star());
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Populate Stars Array for (let i = 0; i < numStars; i++) { stars.push(new Star()); }

Creates numStars new Star objects and adds them to the stars array so draw() has something to animate

createCanvas(windowWidth, windowHeight);
Creates a canvas that exactly fills the browser window, using p5's built-in windowWidth and windowHeight variables.
noStroke();
Turns off outlines for every shape drawn afterward, so stars and their glow appear as soft filled dots rather than dots with borders.
for (let i = 0; i < numStars; i++) {
Loops numStars times (400 by default) to build the initial batch of stars.
stars.push(new Star());
Creates a brand new Star object (which immediately calls reset() in its constructor) and adds it to the end of the stars array.

draw()

draw() runs continuously (about 60 times per second) and is where all animation happens. Here it does two jobs each frame: fade the canvas slightly (for trails) and update+render every star, which is the standard update/render pattern used in almost every particle system.

🔬 What happens if you comment out background(0, 50) above this loop entirely? The trails never fade - what does the screen look like after a few seconds?

  for (let i = 0; i < stars.length; i++) {
    stars[i].update(); // Update star's position and speed
    stars[i].show();   // Draw the star
  }
function draw() {
  // --- Create Trails Effect (2D Specific) ---
  // Draw a semi-transparent black rectangle over the entire canvas.
  // This will fade out the previous frame's stars, creating the trail effect.
  background(0, 50); 

  // --- Update and Draw Stars ---
  for (let i = 0; i < stars.length; i++) {
    stars[i].update(); // Update star's position and speed
    stars[i].show();   // Draw the star
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Update & Draw All Stars for (let i = 0; i < stars.length; i++) { stars[i].update(); stars[i].show(); }

Every frame, moves each star closer to the camera and then renders it at its new position

background(0, 50);
Draws a black rectangle over the whole canvas but with alpha 50 (out of 255), so it only partially covers what was drawn last frame. Because it never fully erases, older star positions linger and fade, producing the streaking trail effect.
for (let i = 0; i < stars.length; i++) {
Loops through every star currently in the stars array, regardless of how many there are.
stars[i].update();
Calls the star's update() method, which moves it closer to the viewer and speeds it up.
stars[i].show();
Calls the star's show() method, which calculates its on-screen position/size and draws it with its glow.

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window changes size. It's the standard place to call resizeCanvas() and handle any layout-dependent state, like this sketch's star positions which are calculated relative to width/2 and height/2.

🔬 This wipes out and rebuilds every star on resize. What do you think would happen visually if you deleted these three lines and only kept resizeCanvas()?

  stars = [];
  for (let i = 0; i < numStars; i++) {
    stars.push(new Star());
  }
function windowResized() {
  // Resize the canvas when the window is resized
  resizeCanvas(windowWidth, windowHeight);
  
  // Re-initialize stars to adapt to new dimensions (optional, but good for fresh start)
  stars = [];
  for (let i = 0; i < numStars; i++) {
    stars.push(new Star());
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Rebuild Stars Array for (let i = 0; i < numStars; i++) { stars.push(new Star()); }

Recreates all stars from scratch so they're centered and scaled correctly for the new canvas size

resizeCanvas(windowWidth, windowHeight);
A built-in p5.js function that changes the canvas's actual pixel dimensions to match the current browser window size.
stars = [];
Empties the stars array completely, discarding every existing star.
for (let i = 0; i < numStars; i++) {
Loops numStars times to rebuild a fresh batch of stars for the new canvas dimensions.
stars.push(new Star());
Creates and adds a new Star, which will use the updated width/height when it calls reset() internally.

constructor()

In JavaScript classes, the constructor runs automatically every time you write `new Star()`. Keeping it tiny and delegating to reset() is a nice pattern - it avoids duplicating initialization code between 'create' and 'respawn' logic.

  constructor() {
    this.reset();
  }
Line-by-line explanation (1 lines)
this.reset();
Immediately calls the reset() method to give this new star all of its starting values (depth, speed, color, size, direction). Reusing reset() here means the exact same setup logic runs both when a star is first created and later whenever it needs to respawn.

reset()

reset() is the 'birth' function for a star - it runs both when a star is first created and every time it needs to respawn after passing the camera. Centralizing all the random starting values here keeps the Star class easy to reason about.

🔬 This ternary picks white 50% of the time. What happens if you change 0.5 to 0.9, so only 10% of stars are white and 90% are blue?

    // Randomize star color between white and a subtle blue
    this.color = random() > 0.5 ? color(255) : color(100, 100, 255); 
  reset() {
    // Abstract "depth" value: starts far away (large z) and moves towards 0 (close)
    this.z = random(width); 
    
    // Initial position is the center of the canvas
    this.x = width / 2;
    this.y = height / 2;

    // Initial speed at which z decreases
    this.speed = random(1, 5); 
    
    // Randomize star color between white and a subtle blue
    this.color = random() > 0.5 ? color(255) : color(100, 100, 255); 
    
    // Base size for perspective scaling
    this.baseSize = random(0.5, 2); 
    
    // Random direction vector away from the center
    this.dir = p5.Vector.random2D();
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Random Star Color this.color = random() > 0.5 ? color(255) : color(100, 100, 255);

Picks either pure white or a subtle blue for this star's color, roughly 50/50

this.z = random(width);
Gives the star a random 'depth' between 0 and the canvas width - this fake z coordinate stands in for how far away the star is.
this.x = width / 2;
All stars start their journey from the horizontal center of the canvas.
this.y = height / 2;
All stars start their journey from the vertical center of the canvas.
this.speed = random(1, 5);
Assigns a random starting speed between 1 and 5, controlling how fast z decreases each frame.
this.color = random() > 0.5 ? color(255) : color(100, 100, 255);
Flips a coin (random() > 0.5) to decide whether this star is white or a pale blue, adding subtle color variety to the field.
this.baseSize = random(0.5, 2);
Picks a random base size multiplier so not every star ends up exactly the same size on screen.
this.dir = p5.Vector.random2D();
Creates a random 2D unit vector pointing in any direction - this determines which way the star will fly outward from the center.

update()

update() is where a star's physics live: it moves through its fake depth dimension, speeds up over time, and respawns itself when it 'reaches' the viewer. This update/respawn pattern is standard in particle systems - it lets a fixed-size array of objects simulate an endless stream of new particles.

🔬 This is the moment a star 'passes' the camera and respawns. What happens if you change the threshold to this.z <= 100 - do stars vanish before reaching full size?

    if (this.z <= 0) {
      this.reset();
    }
  update() {
    // Decrease the abstract z value (star moves closer)
    this.z -= this.speed;

    // Acceleration: increase speed as stars get "closer" (z approaches 0)
    // The closer the star, the more it accelerates.
    this.speed += starAcceleration * (1 - this.z / width);
    
    // Constrain speed to a reasonable range
    this.speed = constrain(this.speed, 1, 30); 

    // If star has passed the "camera" (z <= 0), reset it
    if (this.z <= 0) {
      this.reset();
    }
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Respawn Star Past Camera if (this.z <= 0) { this.reset(); }

Detects when a star has 'reached' the viewer and gives it a fresh random starting depth, speed, color, and direction

this.z -= this.speed;
Moves the star closer to the camera by shrinking its depth value using its current speed.
this.speed += starAcceleration * (1 - this.z / width);
Increases the star's speed slightly every frame. Because (1 - this.z / width) grows larger as z shrinks, stars accelerate more the closer they get - simulating the classic warp-speed rush.
this.speed = constrain(this.speed, 1, 30);
Clamps speed between 1 and 30 so it never becomes negative or absurdly fast, keeping the animation stable.
if (this.z <= 0) {
Checks whether the star's depth has reached zero or below, meaning it has effectively flown past the viewer.
this.reset();
Gives the star a brand-new random depth, speed, color, size, and direction so it can start a fresh journey from the center.

show()

show() is the rendering half of the particle system - it never changes any state, only reads it and draws pixels. Separating 'update logic' (in update()) from 'draw logic' (in show()) is a common and very reusable pattern in creative coding.

🔬 This loop draws 3 glow layers. What happens if you change the loop to run i <= 8 instead - does the halo look thicker and smoother, and does it slow the frame rate?

    for (let i = 1; i <= 3; i++) {
      fill(glowColor);
      ellipse(screenX, screenY, screenRadius * (1 + i * 0.2)); // Gradually larger ellipses for glow
    }
  show() {
    // Calculate screen position and size based on perspective projection
    // As z approaches 0, the star gets larger and moves further from the center
    
    // Map z to an inverse scale: smaller z means closer, larger on screen
    const mappedZ = map(this.z, 0, width, width, 0.1); 

    // Scale the star's radius based on its depth (mappedZ)
    // The `0.01` multiplier fine-tunes the base size
    const screenRadius = this.baseSize * (width / mappedZ) * 0.01; 

    // Calculate screen X and Y positions relative to the center and mappedZ
    const screenX = this.x + this.dir.x * (width / mappedZ);
    const screenY = this.y + this.dir.y * (height / mappedZ);

    // --- Draw Glow Effect ---
    // Create a glow color with low alpha
    let glowColor = color(red(this.color), green(this.color), blue(this.color), 50); 
    for (let i = 1; i <= 3; i++) {
      fill(glowColor);
      ellipse(screenX, screenY, screenRadius * (1 + i * 0.2)); // Gradually larger ellipses for glow
    }
    
    // --- Draw Main Star ---
    fill(this.color); // Set star color
    ellipse(screenX, screenY, screenRadius);
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Draw Glow Halo for (let i = 1; i <= 3; i++) { fill(glowColor); ellipse(screenX, screenY, screenRadius * (1 + i * 0.2)); }

Draws three progressively larger, low-alpha ellipses behind the star to create a soft glowing halo

const mappedZ = map(this.z, 0, width, width, 0.1);
Remaps the star's depth value into an inverse scale used for perspective math - map() converts a number from one range into a proportional value in another range.
const screenRadius = this.baseSize * (width / mappedZ) * 0.01;
Computes how big the star should appear on screen, combining its random base size with a perspective factor (width / mappedZ) and a fine-tuning multiplier.
const screenX = this.x + this.dir.x * (width / mappedZ);
Calculates the star's horizontal screen position by pushing it away from the center along its stored direction vector, scaled by the same perspective factor.
const screenY = this.y + this.dir.y * (height / mappedZ);
Calculates the star's vertical screen position the same way, using the direction vector's y component.
let glowColor = color(red(this.color), green(this.color), blue(this.color), 50);
Builds a new color that matches the star's own RGB values but with a low alpha (50), so it appears as a soft translucent glow rather than a solid shape.
for (let i = 1; i <= 3; i++) {
Loops three times to draw three glow layers of increasing size behind the main star.
ellipse(screenX, screenY, screenRadius * (1 + i * 0.2));
Draws each glow ellipse slightly bigger than the last (20% bigger per layer), stacking translucent circles to fake a soft halo.
fill(this.color);
Switches the fill color to the star's own solid color for the final, sharp main dot.
ellipse(screenX, screenY, screenRadius);
Draws the crisp main star on top of its glow at the calculated screen position and size.

📦 Key Variables

stars array

Holds every Star object currently in the scene; draw() loops over this array each frame to update and render the whole field.

let stars = [];
numStars number

Fixed count of how many stars exist in the field at once - used in setup() and windowResized() to size the stars array.

const numStars = 400;
starAcceleration number

A small constant that controls how quickly each star's speed grows as it nears the camera, driving the warp-speed acceleration feel.

const starAcceleration = 0.005;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG show() - const mappedZ = map(this.z, 0, width, width, 0.1);

The map() output range seems inverted for the intended effect: as this.z approaches 0 (a star reaching the camera), mappedZ approaches width, which makes width/mappedZ shrink toward 1 - so screenRadius and screen displacement actually get SMALLER right before a star respawns, instead of ballooning outward as a true warp-speed effect would.

💡 Try swapping the output range to map(this.z, 0, width, 0.1, width) so mappedZ shrinks as z approaches 0, making width/mappedZ (and therefore size and displacement) grow large right before the star resets - this should make the zoom-past-camera moment much more dramatic.

PERFORMANCE show()

Every star draws 4 ellipses per frame (3 glow layers + 1 main star), so with 400 stars that's 1,600 ellipse() calls every frame, which can get expensive on lower-end devices or if numStars is increased.

💡 Consider pre-rendering the glow as a single semi-transparent sprite with createGraphics() and drawing it with image() instead of three separate ellipse() calls, or reduce the glow loop to 1-2 layers on mobile.

STYLE Star class (reset, update, show)

Several 'magic numbers' are scattered through the class (0.01 size multiplier, 50 glow alpha, 1/30 speed clamp, 0.5/2 base size range), making it hard to tune the look without hunting through methods.

💡 Pull these into named constants near the top of the file (e.g. const SIZE_SCALE = 0.01, const GLOW_ALPHA = 50) so all the visual tuning knobs live in one obvious place.

FEATURE draw() / Star class

The star field always radiates from the exact center of the canvas and never responds to the viewer, which limits interactivity.

💡 Try setting this.x and this.y to mouseX/mouseY when the mouse is pressed, or use it as the origin point so the warp effect can be steered interactively.

🔄 Code Flow

Code flow showing setup, draw, windowresized, constructor, reset, update, show

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

graph TD start[Start] --> setup[setup] setup --> setup-star-loop[setup-star-loop] setup-star-loop --> draw[draw loop] click setup href "#fn-setup" click setup-star-loop href "#sub-setup-star-loop" draw --> draw-star-loop[draw-star-loop] draw --> glow-loop[glow-loop] draw --> respawn-check[respawn-check] draw-star-loop --> update[update] draw-star-loop --> show[show] click draw href "#fn-draw" click draw-star-loop href "#sub-draw-star-loop" click glow-loop href "#sub-glow-loop" click respawn-check href "#sub-respawn-check" update --> respawn-check update --> color-ternary[color-ternary] click color-ternary href "#sub-color-ternary" glow-loop --> show show --> draw-star-loop windowresized[windowresized] --> resize-star-loop[resize-star-loop] resize-star-loop --> setup-star-loop click windowresized href "#fn-windowresized" click resize-star-loop href "#sub-resize-star-loop"

❓ Frequently Asked Questions

What visual effect does the Starfield Attempt - XeLseDai sketch create?

This sketch generates a captivating starfield warp speed effect, where stars fly outward from the center of the canvas, leaving behind glowing trails.

Can users interact with the Starfield Attempt - XeLseDai sketch?

The sketch is not interactive; it automatically animates the stars without user input, providing a mesmerizing visual experience.

What creative coding technique is demonstrated in the Starfield Attempt - XeLseDai sketch?

The sketch showcases the use of 2D trails and perspective scaling to simulate depth and motion in a starfield environment.

Preview

Starfield Attempt - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Starfield Attempt - xelsed.ai - Code flow showing setup, draw, windowresized, constructor, reset, update, show
Code Flow Diagram