AI Magic 8-Ball - Shake for Mystical Answers

This sketch turns a plain canvas into a glowing, interactive Magic 8-Ball set against a twinkling starfield. Clicking inside the ball makes it wobble like it's being shaken, then a random fortune fades into view inside a translucent blue triangle.

🧪 Try This!

Experiment with the code by making these changes:

  1. Supersize the answer text — Increasing textSize() makes the fortune text much bolder and easier to read inside the triangle.
  2. Make the shake wilder — Boosting the rotation multiplier makes the ball wobble much more dramatically while shaking.
  3. Turn up the glow — Raising shadowBlur wraps the ball in a much softer, foggier blue halo.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates the classic toy Magic 8-Ball as an interactive p5.js animation: a glowing black sphere sits over a softly twinkling starfield, and clicking it triggers a wobbling 'shake' before a random mystical answer fades into a blue triangular window. It leans on a handful of core p5.js ideas - the continuous draw() loop, translate() and push()/pop() for local coordinate systems, sin() for organic-feeling motion, alpha blending for fades, and mousePressed() combined with dist() for circular hit-testing.

The code is compact: setup() builds the canvas and scatters star objects into an array, draw() repaints the starfield and the ball every frame while checking a simple state machine (shaking vs. showing an answer), mousePressed() detects clicks inside the ball and kicks off a new shake, and windowResized() keeps everything responsive when the browser is resized. Studying it teaches you how a few booleans and counters (shake, t, fade) are enough to drive a convincing multi-stage animation without any external animation library.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, configures centered text, and scatters 180 star objects at random x/y positions into the stars array.
  2. Every frame, draw() repaints a dark navy background and redraws each star as a point whose brightness pulses using sin(frameCount + star.x), creating a shimmering twinkle effect.
  3. draw() then computes the ball's size and center, translates the origin to the ball's center, and checks the shake boolean: if true it increments a timer t and rotates the ball with a sine wave to simulate shaking, turning shake off after 45 frames.
  4. Once shaking stops and an answer string exists, the fade variable climbs toward 255 each frame, smoothly fading the answer text into view inside the glowing blue triangle drawn with triangle().
  5. When the user clicks, mousePressed() measures the distance from the click to the ball's center with dist(); if the click landed inside the ball, it sets shake to true, resets the timer and fade, and picks a brand-new random reply from the replies array.
  6. If the browser window is resized, windowResized() resizes the canvas and regenerates the entire stars array so the starfield still fills the new canvas dimensions.

🎓 Concepts You'll Learn

Animation state machine (shake vs. reveal)Mouse interaction with dist() hit-testingTrigonometric motion with sin()Alpha fading for smooth revealspush()/pop() and translate() for local coordinatesCanvas responsiveness with windowResized()Glow effects via drawingContext.shadowBlur

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts, making it the right place to configure canvas size, drawing defaults, and to build any starting data (like the stars array) that draw() will use repeatedly.

function setup() {
  createCanvas(windowWidth, windowHeight); noStroke(); textAlign(CENTER, CENTER); textSize(26);
  for (let i = 0; i < 180; i++) stars.push({ x: random(width), y: random(height) });
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Star Field Creation for (let i = 0; i < 180; i++) stars.push({ x: random(width), y: random(height) });

Creates 180 star objects at random positions and stores them in the global stars array so draw() can render them every frame.

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, so the 8-ball scene always spans the whole screen.
noStroke();
Turns off outlines by default for shapes drawn later, like the ball and triangle (stars turn stroke back on themselves in draw()).
textAlign(CENTER, CENTER);
Makes any text() call center itself horizontally and vertically on the coordinates given, which is exactly what's needed for the answer text inside the triangle.
textSize(26);
Sets the default font size in pixels used whenever text() is called.
for (let i = 0; i < 180; i++) stars.push({ x: random(width), y: random(height) });
Runs 180 times, each time creating a plain object with random x and y coordinates and adding it to the stars array.

draw()

draw() runs continuously at roughly 60 frames per second. This sketch shows how a couple of simple state variables (shake, t, fade) combined with sin() and alpha values are enough to build a multi-stage animation - wobble, then fade-in - entirely inside one loop.

🔬 This loop makes stars twinkle using a sine wave on their alpha. What happens if you change 0.02 to 0.1 (faster twinkle) or change 60 to 120 (much bigger brightness swings, possibly going negative)?

for (let s of stars) { stroke(255, 255, 255, 120 + sin((frameCount + s.x) * 0.02) * 60); point(s.x, s.y); }

🔬 This one line controls the whole shake animation. What happens if you change t > 45 to t > 120 (a much longer shake) or change 0.3 to 0.9 (a far wilder wobble)?

if (shake) { t++; rotate(sin(t * 0.4) * 0.3); if (t > 45) shake = false; } else if (ans) fade = min(255, fade + 10);
function draw() {
  background(5, 5, 20); strokeWeight(2);
  for (let s of stars) { stroke(255, 255, 255, 120 + sin((frameCount + s.x) * 0.02) * 60); point(s.x, s.y); }
  noStroke(); let d = min(width, height) * 0.55, cx = width / 2, cy = height / 2;
  push(); translate(cx, cy);
  if (shake) { t++; rotate(sin(t * 0.4) * 0.3); if (t > 45) shake = false; } else if (ans) fade = min(255, fade + 10);
  drawingContext.shadowBlur = 45; drawingContext.shadowColor = 'rgba(0,150,255,0.5)'; fill(0); ellipse(0, 0, d * 1.08);
  drawingContext.shadowBlur = 0; fill(0); ellipse(0, 0, d);
  fill(20, 100, 220); let h = d * 0.42; triangle(-h, h * 0.5, h, h * 0.5, 0, -h * 0.7);
  if (ans && !shake) { fill(255, fade); text(ans, 0, 0); } pop();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Twinkling Star Field for (let s of stars) { stroke(255, 255, 255, 120 + sin((frameCount + s.x) * 0.02) * 60); point(s.x, s.y); }

Draws every star as a point whose alpha oscillates over time using sin(), creating a shimmering twinkle effect.

conditional Shake or Fade State Machine if (shake) { t++; rotate(sin(t * 0.4) * 0.3); if (t > 45) shake = false; } else if (ans) fade = min(255, fade + 10);

Decides each frame whether the ball should still be wobbling (shake) or, once shaking is done and an answer exists, whether the text should keep fading in.

conditional Answer Text Reveal if (ans && !shake) { fill(255, fade); text(ans, 0, 0); } pop();

Only draws the answer text once there IS an answer and the shake animation has finished, using the fade value as the text's opacity.

background(5, 5, 20); strokeWeight(2);
Repaints the whole canvas with a near-black navy color every frame (erasing the previous frame) and sets how thick strokes will be drawn, used for the star points.
for (let s of stars) { stroke(255, 255, 255, 120 + sin((frameCount + s.x) * 0.02) * 60); point(s.x, s.y); }
Loops through every star and draws it as a single white point, using sin() with frameCount to make its transparency rise and fall smoothly over time - the twinkle effect.
noStroke(); let d = min(width, height) * 0.55, cx = width / 2, cy = height / 2;
Turns stroke back off for the ball shapes, then computes the ball's diameter as 55% of whichever canvas dimension is smaller (so it always fits), plus the canvas center coordinates.
push(); translate(cx, cy);
Saves the current drawing state and shifts the coordinate origin (0,0) to the center of the ball, so all following shapes can be drawn relative to the ball's middle instead of the canvas corner.
if (shake) { t++; rotate(sin(t * 0.4) * 0.3); if (t > 45) shake = false; } else if (ans) fade = min(255, fade + 10);
If the ball is currently shaking, increments the timer t and rotates the whole ball by a sine-wave angle that swings back and forth, stopping the shake after 45 frames; otherwise, if there's already an answer to show, gradually increases fade toward full opacity.
drawingContext.shadowBlur = 45; drawingContext.shadowColor = 'rgba(0,150,255,0.5)'; fill(0); ellipse(0, 0, d * 1.08);
Uses the underlying canvas API (drawingContext) to add a soft blue drop-shadow, then draws a slightly larger black circle behind the ball to create a glowing halo.
drawingContext.shadowBlur = 0; fill(0); ellipse(0, 0, d);
Turns the glow effect off so it doesn't affect later shapes, then draws the solid black ball itself on top of the glow.
fill(20, 100, 220); let h = d * 0.42; triangle(-h, h * 0.5, h, h * 0.5, 0, -h * 0.7);
Sets a blue fill color, computes a size h relative to the ball's diameter, then draws the classic upside-down triangle window where the answer appears.
if (ans && !shake) { fill(255, fade); text(ans, 0, 0); } pop();
If there is an answer and the shake has finished, draws the answer text in white using fade as its alpha (so it fades in), then pop() restores the coordinate system back to normal for the next frame.

mousePressed()

mousePressed() is a p5.js event function that automatically runs once every time the mouse button is clicked. Combined with dist(), it's the standard way to detect clicks on circular objects since dist() gives you the exact radius-based test you need.

🔬 This is the click hit-test. What happens if you shrink d / 2 down to d / 4 - can you still click the ball easily, or do you now have to click very precisely near its center?

if (dist(mouseX, mouseY, cx, cy) < d / 2) { shake = true; t = 0; fade = 0; ans = random(replies); }
function mousePressed() {
  let d = min(width, height) * 0.55, cx = width / 2, cy = height / 2;
  if (dist(mouseX, mouseY, cx, cy) < d / 2) { shake = true; t = 0; fade = 0; ans = random(replies); }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Click-Inside-Ball Check if (dist(mouseX, mouseY, cx, cy) < d / 2) { shake = true; t = 0; fade = 0; ans = random(replies); }

Uses dist() to measure how far the click was from the ball's center, and only triggers a new shake/answer if the click landed within the ball's radius.

let d = min(width, height) * 0.55, cx = width / 2, cy = height / 2;
Recomputes the ball's diameter and center point the same way draw() does, so the click detection matches exactly where the ball is drawn.
if (dist(mouseX, mouseY, cx, cy) < d / 2) { shake = true; t = 0; fade = 0; ans = random(replies); }
dist() measures the straight-line distance between the mouse click and the ball's center; if that distance is less than the ball's radius (d / 2), the click was inside the ball, so shake starts, the timer and fade reset to zero, and a brand-new random answer is picked from replies.

windowResized()

windowResized() is a p5.js event function that fires automatically whenever the browser window changes size. It's the standard place to call resizeCanvas() and refresh any position data (like star coordinates) that depended on the old canvas dimensions.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); stars = [];
  for (let i = 0; i < 180; i++) stars.push({ x: random(width), y: random(height) });
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

for-loop Star Field Rebuild for (let i = 0; i < 180; i++) stars.push({ x: random(width), y: random(height) });

Refills the stars array with 180 new random positions that fit the freshly resized canvas dimensions.

resizeCanvas(windowWidth, windowHeight); stars = [];
Resizes the canvas to match the browser window's new dimensions, then empties the stars array since the old star positions no longer match the new canvas size.
for (let i = 0; i < 180; i++) stars.push({ x: random(width), y: random(height) });
Rebuilds the starfield from scratch with 180 new randomly placed stars that fit within the newly resized width and height.

📦 Key Variables

stars array

Holds one object per background star, each with random x/y coordinates, so draw() can render and animate the whole starfield every frame.

let stars = [];
ans string

Stores the currently chosen fortune-telling reply (empty string until the ball is clicked for the first time).

let ans = '';
shake boolean

Tracks whether the ball is currently in its wobbling shake animation; controls which branch of the state machine in draw() runs.

let shake = false;
t number

A frame counter used only during the shake animation to drive the sine-wave rotation and to know when 45 frames have passed and the shake should stop.

let t = 0;
fade number

Tracks the current opacity (0-255) of the answer text, incrementing each frame after a shake finishes so the text fades smoothly into view.

let fade = 0;
replies array

A fixed list of classic Magic 8-Ball answers that random() picks from whenever the ball is clicked.

const replies = ['Yes','No','Ask again later','Definitely','My sources say no','Outlook good','Very doubtful','It is certain'];

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG mousePressed()

Clicking the ball again while it's already mid-shake immediately overwrites the current answer and restarts the shake timer with no cooldown, which can make rapid clicking look glitchy or unresponsive.

💡 Guard the click handler with `if (!shake) { ... }` so a new shake can only begin once the current animation has finished.

PERFORMANCE windowResized()

Every resize throws away and completely rebuilds the stars array with brand-new random positions, so the entire starfield visibly jumps around instead of just repositioning to fit the new size.

💡 Store each star's x/y as a fraction of width/height (e.g. star.xPct = random(1)) and multiply by the current width/height when drawing, so stars stay visually stable across resizes.

STYLE draw()

Most lines cram several unrelated statements together (canvas clearing, star drawing, ball drawing, and text all mixed into a handful of dense lines), which makes the function hard to read and debug.

💡 Split the logic into small helper functions like drawStars(), drawBall(d), and drawAnswer(fade), then call them in order from draw() for much clearer code.

FEATURE mousePressed()

There's no visual cue (like a cursor change) to tell the user the ball is clickable, so first-time visitors might not realize they should click it.

💡 In draw(), check the same dist() test used in mousePressed() and call cursor(HAND) when the mouse hovers over the ball, reverting to cursor(ARROW) otherwise.

🔄 Code Flow

Code flow showing setup, draw, mousepressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> starcreation[Star Creation Loop] draw --> startwobble[Shake or Fade State Machine] draw --> starttwinkle[Twinkling Star Field] draw --> answercheck[Answer Text Reveal] click setup href "#fn-setup" click starcreation href "#sub-star-creation-loop" click startwobble href "#sub-shake-fade-conditional" click starttwinkle href "#sub-star-twinkle-loop" click answercheck href "#sub-answer-text-conditional" starcreation --> starloop[For Loop: Create Stars] starloop --> stararray[Store in Stars Array] starloop --> draw starttwinkle --> twinkleloop[For Loop: Twinkle Stars] twinkleloop --> draw startwobble --> shakecheck[Check Shake State] shakecheck -->|is shaking| draw shakecheck -->|not shaking| fadecheck[Check Fade State] fadecheck --> answercheck answercheck -->|has answer| draw answercheck -->|no answer| draw draw --> mousecheck[Mouse Hit Test] mousecheck -->|click inside ball| shake[Trigger Shake/Answer] mousecheck -->|click outside ball| draw setup --> windowresize[windowResized] windowresize --> starrepopulate[Star Repopulate Loop] starrepopulate --> starloop2[For Loop: Refill Stars Array] starloop2 --> draw click mousecheck href "#sub-mouse-hit-test" click windowresize href "#fn-windowresized" click starrepopulate href "#sub-star-repopulate-loop"

❓ Frequently Asked Questions

What visual experience does the AI Magic 8-Ball sketch offer?

The sketch features a starry mystical background with glowing blue triangles that display classic fortune-telling answers in a visually engaging way.

How can users interact with the AI Magic 8-Ball sketch?

Users can click to shake the magic 8-ball and receive a random yes/no answer to their question, experiencing a fun and interactive fortune-telling moment.

What creative coding concepts are showcased in this p5.js sketch?

This sketch demonstrates techniques such as randomization for generating answers, mouse interaction for triggering events, and dynamic visual effects using shapes and shadows.

Preview

AI Magic 8-Ball - Shake for Mystical Answers - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Magic 8-Ball - Shake for Mystical Answers - Code flow showing setup, draw, mousepressed, windowresized
Code Flow Diagram