AI Bouncing Ball V2 - Xelsed p5flash-alpha02

This sketch animates a glowing ball that bounces around a full-window canvas, pulsing in size with a sine wave, casting a soft layered shadow, and flashing a new vibrant HSB color every time it hits an edge. A semi-transparent background creates a smooth motion trail instead of harsh redraws.

🧪 Try This!

Experiment with the code by making these changes:

  1. Freeze the trail into permanent streaks — Lowering the background alpha even further makes old frames linger almost forever, turning the bounce path into a glowing streak painting.
  2. Make the ball pulse dramatically — Increasing the pulse amplitude from 0.15 to a much larger fraction makes the ball balloon and shrink noticeably instead of subtly breathing.
  3. Speed up the ball dramatically — Widening the random velocity range at setup makes the ball zip around much faster and bounce more chaotically.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch takes the classic bouncing ball demo and layers on several polish techniques: a sine-wave pulse for the ball's size, a soft multi-circle shadow drawn underneath it, a fading trail effect from a semi-transparent background, and vivid random colors triggered by HSB color mode whenever the ball bounces. It's a great sketch to study because it shows how a handful of simple math tricks - sin(), random(), and alpha blending - combine to make a static shape feel alive.

The code has just two core functions: setup() initializes the canvas, color mode, and the ball's starting position/velocity/color, while draw() runs every frame to compute the pulsing size, draw the shadow, move the ball, check for bounces, and render the ball itself. A third function, windowResized(), keeps the canvas matching the browser window. Studying this sketch teaches you how to fake depth with layered transparent shapes, how sine waves create smooth oscillation, and how HSB color mode makes random-but-vibrant colors easy.

⚙️ How It Works

  1. When the page loads, setup() creates a canvas that fills the browser window, switches to HSB color mode, centers the ball, and gives it a random velocity and a random vibrant color
  2. Every frame, draw() paints a nearly-black semi-transparent rectangle over the whole canvas instead of a solid background, which lets old frames fade out slowly and creates a motion trail
  3. The ball's current size is recalculated each frame using sin(frameCount * 0.05), making it smoothly grow and shrink as if breathing
  4. A loop draws three overlapping, increasingly large and increasingly transparent black circles offset below the ball to fake a soft blurred shadow
  5. The ball's x and y positions are updated by adding its velocity, and if the ball's edge crosses a canvas boundary, that axis's velocity is flipped (reversed) and a brand new random HSB color is assigned
  6. Finally the ball is drawn with its current color and pulsing size, and windowResized() keeps the canvas matching the browser size whenever the window changes

🎓 Concepts You'll Learn

HSB color modeSine wave animationAlpha transparency for trailsCollision/bounce detectionLayered shapes for fake depthwindowResized responsiveness

📝 Code Breakdown

setup()

setup() runs once when the sketch starts, and is the right place to configure the canvas, color settings, and initial values for any variable the rest of the sketch depends on.

function setup() {
  // Create a canvas that fills the entire browser window
  createCanvas(windowWidth, windowHeight);

  // Set color mode to HSB for more vibrant and intuitive color control
  // HSB: Hue (0-255), Saturation (0-255), Brightness (0-255)
  colorMode(HSB, 255); 

  // Initialize ball position to the center of the canvas
  x = width / 2;
  y = height / 2;

  // Initialize velocity with random values for varied movement
  // The ball will move between -5 and 5 pixels per frame in both x and y directions
  vx = random(-5, 5); 
  vy = random(-5, 5); 

  // Initialize the ball's color with a random vibrant hue, high saturation, and full brightness
  // HSB ensures colors are consistently bright and saturated.
  currentColor = color(random(255), 200, 255); // Random hue, high saturation (200/255), full brightness (255/255)

  // Disable drawing outlines around shapes for a cleaner look
  noStroke(); 
}
Line-by-line explanation (8 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that exactly matches the browser window's current width and height.
colorMode(HSB, 255);
Switches p5's color system from default RGB to Hue-Saturation-Brightness, with each channel ranging 0-255, making it easy to generate vibrant colors just by randomizing hue.
x = width / 2;
Places the ball's starting x-position exactly in the horizontal center of the canvas.
y = height / 2;
Places the ball's starting y-position exactly in the vertical center of the canvas.
vx = random(-5, 5);
Picks a random horizontal speed between -5 and 5, so the ball starts moving in an unpredictable direction.
vy = random(-5, 5);
Picks a random vertical speed between -5 and 5 for varied initial motion.
currentColor = color(random(255), 200, 255); // Random hue, high saturation (200/255), full brightness (255/255)
Builds a color object with a random hue but fixed high saturation and full brightness, guaranteeing the color always looks vivid rather than washed out or dark.
noStroke();
Turns off shape outlines so circles are drawn as solid fills without a border line.

draw()

draw() runs continuously (about 60 times per second by default) and is where all per-frame logic lives: recalculating values that change over time, drawing layered shapes for depth, updating position with velocity, and checking for collisions. Studying the order of operations here (background fade -> shadow -> move -> bounce-check -> draw ball) shows how layering order affects what appears on top of what.

🔬 This loop draws 3 shadow rings. What happens visually if you change `i < 3` to `i < 8` and reduce the 0.25 step so alpha still reaches near zero by the last ring?

  for (let i = 0; i < 3; i++) {
    // Alpha decreases for outer circles (more diffuse)
    let alphaStep = shadowMaxAlpha * (1 - i * 0.25); 

🔬 This is the horizontal bounce logic. What happens if you multiply vx by -1.1 instead of -1, so the ball gains speed with every bounce instead of staying constant?

  if (x + currentBallSize / 2 > width || x - currentBallSize / 2 < 0) {
    vx *= -1; // Reverse the x-velocity (bounce horizontally)
    
    // Generate a new random vibrant color for the ball on bounce
    currentColor = color(random(255), 200, 255); // Random hue, high saturation, full brightness
  }
function draw() {
  // Create a more visible fading trail effect by drawing a semi-transparent background
  // Using HSB for background: hue 0 (black), saturation 0, brightness 0, alpha 50
  // Alpha (transparency) is set to 50 (out of 255) - slightly higher than previous iteration
  background(0, 0, 0, 50); 

  // --- Ball Size Variation ---
  // Make the ball size vary slightly as it moves using a sine wave
  // The ball will pulse smoothly between baseBallSize and baseBallSize + (baseBallSize * 0.15)
  // The amplitude (0.15) has been increased slightly for a more noticeable pulse.
  currentBallSize = baseBallSize + sin(frameCount * 0.05) * (baseBallSize * 0.15); 

  // --- Refined Subtle Shadow Under the Ball ---
  // Draw the shadow before the ball so it appears underneath
  // We'll draw multiple semi-transparent circles to simulate a softer, blurred shadow edge.
  
  let shadowOffsetX = currentBallSize * 0.1; // Offset in x, based on ball size
  let shadowOffsetY = currentBallSize * 0.15; // Slightly more offset in y for a more pronounced shadow
  let shadowBaseSize = currentBallSize * 1.1; // Base size of the shadow, slightly larger than the ball
  let shadowMaxAlpha = 70; // Maximum alpha for the shadow (out of 255)

  // Draw 3 concentric, semi-transparent circles to create a soft, gradient shadow
  for (let i = 0; i < 3; i++) {
    // Alpha decreases for outer circles (more diffuse)
    let alphaStep = shadowMaxAlpha * (1 - i * 0.25); 
    
    // Size increases for outer circles
    let sizeStep = shadowBaseSize + (i * currentBallSize * 0.07); 
    
    // Offset decreases for inner circles (closer to the ball)
    let offsetStepX = shadowOffsetX * (1 - i * 0.3); 
    let offsetStepY = shadowOffsetY * (1 - i * 0.3);

    fill(0, 0, 0, alphaStep); // Semi-transparent black
    circle(x + offsetStepX, y + offsetStepY, sizeStep); 
  }

  // Update the ball's position based on its velocity
  x += vx; 
  y += vy; 

  // --- Bounce logic for horizontal edges (left and right) ---
  // Check if the ball's edge (x + radius) goes beyond the right edge (width)
  // or if the ball's edge (x - radius) goes beyond the left edge (0)
  if (x + currentBallSize / 2 > width || x - currentBallSize / 2 < 0) {
    vx *= -1; // Reverse the x-velocity (bounce horizontally)
    
    // Generate a new random vibrant color for the ball on bounce
    currentColor = color(random(255), 200, 255); // Random hue, high saturation, full brightness
  }

  // --- Bounce logic for vertical edges (top and bottom) ---
  // Check if the ball's edge (y + radius) goes beyond the bottom edge (height)
  // or if the ball's edge (y - radius) goes beyond the top edge (0)
  if (y + currentBallSize / 2 > height || y - currentBallSize / 2 < 0) {
    vy *= -1; // Reverse the y-velocity (bounce vertically)
    
    // Generate a new random vibrant color for the ball on bounce
    currentColor = color(random(255), 200, 255); // Random hue, high saturation, full brightness
  }

  // Set the fill color for the ball
  fill(currentColor); 

  // Draw the ball as a circle at its current position with its current size
  circle(x, y, currentBallSize); 
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

for-loop Layered Shadow Loop for (let i = 0; i < 3; i++) {

Draws 3 concentric semi-transparent black circles with increasing size and decreasing alpha/offset to fake a soft blurred shadow beneath the ball

conditional Horizontal Bounce Check if (x + currentBallSize / 2 > width || x - currentBallSize / 2 < 0) {

Detects when the ball's edge crosses the left or right canvas boundary, reverses vx, and assigns a new random color

conditional Vertical Bounce Check if (y + currentBallSize / 2 > height || y - currentBallSize / 2 < 0) {

Detects when the ball's edge crosses the top or bottom canvas boundary, reverses vy, and assigns a new random color

background(0, 0, 0, 50);
Draws a nearly-black rectangle over the whole canvas with low opacity (50 out of 255) instead of fully clearing it, so previous frames fade slowly instead of vanishing - this is what creates the trailing effect.
currentBallSize = baseBallSize + sin(frameCount * 0.05) * (baseBallSize * 0.15);
Uses sin() with the ever-increasing frameCount to produce a smooth wave between -1 and 1, scaled to plus/minus 15% of the base size, making the ball rhythmically grow and shrink.
let shadowOffsetX = currentBallSize * 0.1; // Offset in x, based on ball size
Calculates how far right the shadow is offset, scaling with the ball's current pulsing size.
let shadowOffsetY = currentBallSize * 0.15; // Slightly more offset in y for a more pronounced shadow
Calculates how far down the shadow is offset - larger than the x-offset to simulate light coming from above.
let shadowBaseSize = currentBallSize * 1.1; // Base size of the shadow, slightly larger than the ball
Makes the shadow's base diameter 10% bigger than the ball so it peeks out visibly around the edges.
let shadowMaxAlpha = 70; // Maximum alpha for the shadow (out of 255)
Sets the darkest opacity the shadow's innermost circle will use.
let alphaStep = shadowMaxAlpha * (1 - i * 0.25);
For each loop iteration i (0,1,2), reduces the alpha so outer shadow rings are more transparent, creating a soft gradient fade.
let sizeStep = shadowBaseSize + (i * currentBallSize * 0.07);
Increases the circle's size slightly with each iteration so outer rings are bigger, simulating blur spread.
let offsetStepX = shadowOffsetX * (1 - i * 0.3);
Reduces the x-offset for outer rings so they pull back toward the ball, keeping the shadow shape cohesive.
fill(0, 0, 0, alphaStep); // Semi-transparent black
Sets the fill color to black at the calculated transparency for this shadow ring.
circle(x + offsetStepX, y + offsetStepY, sizeStep);
Draws one shadow ring, offset from the ball's true position and sized according to this loop iteration.
x += vx;
Moves the ball horizontally by adding its velocity to its position - this happens every single frame.
y += vy;
Moves the ball vertically by adding its velocity to its position every frame.
if (x + currentBallSize / 2 > width || x - currentBallSize / 2 < 0) {
Checks whether the ball's right edge has passed the canvas's right side, OR its left edge has passed the canvas's left side - using currentBallSize/2 (the radius) makes the check accurate for the ball's visible edge, not just its center point.
vx *= -1; // Reverse the x-velocity (bounce horizontally)
Flips the sign of the horizontal velocity, which reverses the ball's left/right direction, simulating a bounce.
currentColor = color(random(255), 200, 255); // Random hue, high saturation, full brightness
Generates a brand new vivid color with a random hue every time the ball bounces horizontally.
if (y + currentBallSize / 2 > height || y - currentBallSize / 2 < 0) {
Same edge-check logic as the horizontal case, but for the top and bottom of the canvas.
vy *= -1; // Reverse the y-velocity (bounce vertically)
Flips the vertical velocity's sign so the ball bounces up/down.
fill(currentColor);
Sets the shape fill to the ball's current color before drawing it.
circle(x, y, currentBallSize);
Draws the ball itself as a circle at its updated position, using its pulsing current size.

windowResized()

windowResized() is a special p5.js callback function that fires automatically on browser resize events, letting you keep responsive canvases without manually listening for resize events yourself.

function windowResized() {
  // Resize the canvas to match the new window dimensions
  resizeCanvas(windowWidth, windowHeight); 
  // Optionally, you could reset the ball's position to the center here
  // x = width / 2;
  // y = height / 2;
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Called automatically by p5.js whenever the browser window changes size; this resizes the canvas element to match the new width and height so the sketch always fills the window.

📦 Key Variables

x number

The ball's current horizontal position on the canvas, updated every frame by adding vx

let x;
y number

The ball's current vertical position on the canvas, updated every frame by adding vy

let y;
vx number

The ball's horizontal velocity (pixels per frame); flips sign whenever the ball hits the left or right edge

let vx;
vy number

The ball's vertical velocity (pixels per frame); flips sign whenever the ball hits the top or bottom edge

let vy;
baseBallSize number

The ball's average/base diameter before the sine-wave pulse is applied

let baseBallSize = 50;
currentBallSize number

The ball's actual diameter this frame, recalculated every frame from baseBallSize plus a sine-wave pulse

let currentBallSize;
currentColor object

Stores the p5.Color object currently used to fill the ball; reassigned to a new random vibrant HSB color on every bounce

let currentColor;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG draw() bounce logic

The ball can get its position stuck slightly outside the canvas bounds because velocity is reversed but position isn't clamped back inside the boundary, which can cause jittering or the ball briefly sticking to the edge at high speeds.

💡 After reversing velocity, also clamp the position with something like x = constrain(x, currentBallSize/2, width - currentBallSize/2) to guarantee it's always fully inside the canvas.

STYLE draw()

shadowMaxAlpha is redeclared as a local variable inside draw() every frame even though its value never changes, which is slightly wasteful and also shadows/duplicates naming intent versus a true constant.

💡 Move shadowMaxAlpha (and other truly constant shadow parameters) outside draw() as a global constant declared once with `const`.

PERFORMANCE draw() shadow loop

Three separate fill() and circle() calls run every single frame purely for a static-shaped shadow effect, adding unnecessary draw calls when the visual difference between rings is subtle.

💡 Consider pre-rendering the shadow gradient once into an offscreen createGraphics() buffer and simply drawing that image each frame instead of recomputing three circles every frame.

FEATURE setup()/windowResized()

When the window is resized, the ball's position isn't adjusted, so if the window shrinks, the ball can end up outside the new smaller canvas until its next bounce.

💡 Uncomment and use the x = width/2; y = height/2; lines in windowResized(), or better, use constrain() to keep x and y within the new width/height immediately after resizing.

🔄 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 --> shadowloop[shadow-loop] draw --> horizontalbounce[horizontal-bounce] draw --> verticalbounce[vertical-bounce] shadowloop --> draw horizontalbounce --> draw verticalbounce --> draw click setup href "#fn-setup" click draw href "#fn-draw" click shadowloop href "#sub-shadow-loop" click horizontalbounce href "#sub-horizontal-bounce" click verticalbounce href "#sub-vertical-bounce"

❓ Frequently Asked Questions

What visual effects does the AI Bouncing Ball V2 sketch produce?

The sketch creates a colorful bouncing ball animation with dynamic color changes on each bounce, a smooth fading trail effect, and a responsive canvas that adjusts to the browser window size.

Can users interact with the AI Bouncing Ball V2 sketch, and if so, how?

While the sketch primarily runs autonomously, users can resize their browser window to see the canvas adapt responsively to different dimensions.

What creative coding techniques are showcased in the AI Bouncing Ball V2 sketch?

This sketch demonstrates techniques such as smooth animation, dynamic color manipulation using HSB color mode, and the creation of visual effects like trails and size variation through sine wave oscillation.

Preview

AI Bouncing Ball V2 - Xelsed p5flash-alpha02 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Bouncing Ball V2 - Xelsed p5flash-alpha02 - Code flow showing setup, draw, windowresized
Code Flow Diagram