AI Fortune Cookie - Crack Open Your AI-Generated Destiny

This sketch draws a golden fortune cookie that cracks open when clicked, splitting into two halves while a paper slip grows out from the center. The fortune text and lucky numbers on that slip are generated live by calling OpenAI's GPT-4o-mini API, so every crack reveals a fresh, AI-written fortune.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the cookie pure white — Changing cookieCol instantly recolors the entire cookie shape since it's used as the fill color in drawCookie().
  2. Speed up the crack animation — Increasing the amount t grows by each frame makes the cookie snap open much faster.
  3. Grow a bigger fortune paper — Raising the multiplier on slipT makes the fortune slip expand to a much wider size once fully revealed.
  4. Switch to a cool blue background — bgCol is repainted every frame in draw(), so changing its RGB values instantly recolors the whole scene.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch renders an animated golden fortune cookie that splits into two halves and reveals a paper fortune slip the moment you click it, with the fortune text itself generated in real time by OpenAI's GPT-4o-mini model. The crack animation relies on push()/pop() transform stacking, rotate(), and translate() to swing each cookie half outward, while a single eased timer variable drives both the crack and the slip's growth. Underneath the visuals sits an asynchronous fetch() call to a chat completion API, meaning the drawing loop keeps animating smoothly while the network request resolves in the background.

The code is organized around one global state machine ('closed', 'opening', 'open') and one time variable, t, that everything else derives from. draw() runs every frame to redraw the slip and cookie, drawCookie() and drawSlip() each read t to calculate size, position, and rotation, and mousePressed() triggers the whole sequence by flipping state and calling getFortune(). Studying this sketch teaches you how to structure a state machine, ease animations with a single progress variable, and safely wire an external API call into a p5.js sketch.

⚙️ How It Works

  1. When the page loads, setup() creates a full-window canvas, centers all text, and defines three colors used throughout: the cookie color, its shadow color, and the dark red background.
  2. Every frame, draw() clears the background, shifts the drawing origin to the screen's center, then draws the fortune slip behind the cookie and the cookie itself on top, showing a 'Tap to open' prompt only while the cookie is still closed.
  3. Clicking anywhere triggers mousePressed(), which sets state to 'opening', resets the timer t to 0, and immediately calls getFortune() to start a new API request.
  4. getFortune() first fills in five random placeholder lucky numbers and an '...' fortune, then sends a fetch() request to OpenAI's chat completions endpoint asking for a short, cryptic fortune; when the response returns, the real fortune text replaces the placeholder (or an error message appears if the request fails).
  5. While state is 'opening', drawCookie() increases t by 0.03 every frame until it reaches 1, using that value to rotate, translate, and spread the two cookie halves apart and shrink the little crumb ellipse in the middle.
  6. drawSlip() derives a separate progress value, slipT, from t so the paper slip only starts appearing partway through the crack, then grows in width until the fortune text and lucky numbers become fully visible and readable.

🎓 Concepts You'll Learn

State machinesEasing / progress variablespush() and pop() transform stackingAsynchronous fetch() callsConditional renderingRotation and translation

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts, which makes it the right place to size the canvas and set up variables that don't change every frame, like colors and text settings.

function setup(){createCanvas(windowWidth,windowHeight);textAlign(CENTER,CENTER);textWrap(WORD);
cookieCol=color(240,194,125);shadowCol=color(205,150,80);bgCol=color(40,0,0);}
Line-by-line explanation (6 lines)
createCanvas(windowWidth,windowHeight)
Makes the canvas fill the entire browser window so the sketch looks fullscreen.
textAlign(CENTER,CENTER)
Sets all future text to be centered both horizontally and vertically around the coordinates you give text().
textWrap(WORD)
Makes long fortune strings wrap onto multiple lines at word boundaries instead of overflowing.
cookieCol=color(240,194,125)
Stores a warm tan color used for the top layer of the cookie.
shadowCol=color(205,150,80)
Stores a slightly darker tan used to fake shading behind each cookie half.
bgCol=color(40,0,0)
Stores a dark red background color that gets drawn fresh every frame.

draw()

draw() is the animation loop that p5.js calls automatically about 60 times per second. It's where every frame's drawing decisions get made, driven by the current value of state and t.

function draw(){background(bgCol);translate(width/2,height/2);drawSlip();drawCookie();
fill(255,220);noStroke();textSize(14);if(state==="closed")text("Tap to open your fortune",0,height*0.3-40);}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Show Closed Prompt if(state==="closed")text("Tap to open your fortune",0,height*0.3-40);

Only shows the instructional prompt text while the cookie hasn't been clicked yet.

background(bgCol)
Repaints the whole canvas with the dark red background color every frame, erasing the previous frame's drawing.
translate(width/2,height/2)
Moves the origin (0,0) to the center of the screen so all following shapes can be drawn relative to the middle.
drawSlip()
Draws the fortune paper slip first, so it appears layered behind the cookie halves.
drawCookie()
Draws the cookie on top of the slip and also advances the crack animation timer.
if(state==="closed")text("Tap to open your fortune",0,height*0.3-40)
Displays a hint message only before the user has clicked, telling them what to do.

drawCookie()

This function is the heart of the animation: it both advances the shared timer t and uses it to compute rotation and translation for two mirrored halves, showing how one variable can drive many visual properties at once.

🔬 This loop draws each cookie half as two ellipses sized 140 by 80. What happens if you change those to 200 and 100 to make a bigger, wider cookie?

for(let s of[-1,1]){push();let ang=s*(baseR+spread);translate(s*offset*openT,-10*openT);rotate(ang);
fill(shadowCol);ellipse(0,8,140,80);fill(cookieCol);ellipse(0,0,140,80);pop();}
function drawCookie(){push();noStroke();
let openT=(state==="opening"||state==="open")?t:0;
if(state==="opening"){t=min(1,t+0.03);if(t>=1)state="open";}
let baseR=PI*0.12,spread=openT*PI*0.4,offset=40+openT*70;
for(let s of[-1,1]){push();let ang=s*(baseR+spread);translate(s*offset*openT,-10*openT);rotate(ang);
fill(shadowCol);ellipse(0,8,140,80);fill(cookieCol);ellipse(0,0,140,80);pop();}
fill(190,135,70);ellipse(0,10,60-40*openT,24-16*openT);pop();}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Animate Opening Progress if(state==="opening"){t=min(1,t+0.03);if(t>=1)state="open";}

Increments the animation timer t every frame while cracking, then flips to the finished 'open' state once t hits 1.

for-loop Draw Both Cookie Halves for(let s of[-1,1]){push();let ang=s*(baseR+spread);translate(s*offset*openT,-10*openT);rotate(ang); fill(shadowCol);ellipse(0,8,140,80);fill(cookieCol);ellipse(0,0,140,80);pop();}

Loops over -1 and 1 to mirror the same drawing code for the left and right cookie halves, using the sign to flip direction.

let openT=(state==="opening"||state==="open")?t:0;
Uses the current timer t as the 'openness' amount only while cracking or fully open; otherwise treats the cookie as fully closed (0).
if(state==="opening"){t=min(1,t+0.03);if(t>=1)state="open";}
Each frame while opening, nudges t upward by 0.03 but never past 1, then switches to the 'open' state once the animation finishes.
let baseR=PI*0.12,spread=openT*PI*0.4,offset=40+openT*70;
Calculates the resting rotation angle (baseR), how much extra rotation to add as it opens (spread), and how far to slide each half sideways (offset).
for(let s of[-1,1]){...}
Runs the same block twice, once with s=-1 (left half) and once with s=1 (right half), so both halves mirror each other using the sign.
translate(s*offset*openT,-10*openT);rotate(ang);
Moves this half sideways and slightly up based on how open the cookie is, then rotates it outward.
fill(shadowCol);ellipse(0,8,140,80);fill(cookieCol);ellipse(0,0,140,80);
Draws a darker shadow ellipse slightly below, then the main tan cookie ellipse on top, faking simple shading.
fill(190,135,70);ellipse(0,10,60-40*openT,24-16*openT);
Draws a small brown crumb shape in the center that shrinks as the cookie opens, since the halves are pulling apart.

drawSlip()

drawSlip() shows how to derive a second, independently-timed progress value (slipT) from a shared timer (t), letting different parts of an animation start and finish at different moments.

🔬 This delays the slip's appearance until t passes 0.25. What happens if you change 0.25 to 0, so the slip starts growing immediately alongside the cookie crack?

let slipT=constrain((t-0.25)/0.6,0,1);if(slipT<=0)return;
function drawSlip(){let slipT=constrain((t-0.25)/0.6,0,1);if(slipT<=0)return;
push();rectMode(CENTER);noStroke();let w=260*slipT,h=40;
fill(250);rect(0,40,w,h,6);fill(0);textSize(14);
if(w>40)text(fortune,0,36,w-12,h-8);
if(nums.length&&w>80){textSize(11);text("Lucky: "+nums.join(" "),0,56);}pop();}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Skip Drawing Before Reveal if(slipT<=0)return;

Exits the function early so nothing is drawn until the crack animation has progressed far enough.

conditional Show Fortune Text if(w>40)text(fortune,0,36,w-12,h-8);

Only renders the fortune text once the paper slip is wide enough to hold readable text.

conditional Show Lucky Numbers if(nums.length&&w>80){textSize(11);text("Lucky: "+nums.join(" "),0,56);}

Displays the five lucky numbers once they exist and the slip has grown wide enough to fit them.

let slipT=constrain((t-0.25)/0.6,0,1);
Remaps t so the slip's own progress starts later than the cookie crack (once t passes 0.25) and reaches 1 by t=0.85.
if(slipT<=0)return;
If the slip hasn't started growing yet, skip drawing it entirely this frame.
let w=260*slipT,h=40;
Calculates the slip's current width based on its progress, growing from 0 up to 260 pixels; height stays fixed at 40.
fill(250);rect(0,40,w,h,6);
Draws a near-white rounded rectangle as the paper background, positioned just below center.
if(w>40)text(fortune,0,36,w-12,h-8);
Draws the fortune string inside a text box slightly smaller than the paper, only once there's enough room to read it.
if(nums.length&&w>80){textSize(11);text("Lucky: "+nums.join(" "),0,56);}
Joins the array of lucky numbers into a string and draws it below the fortune, once the paper is wide enough.

mousePressed()

mousePressed() is a built-in p5.js event function that automatically runs whenever the mouse is clicked anywhere on the canvas, making it perfect for simple click-to-trigger interactions like this one.

function mousePressed(){if(state==="opening")return;state="opening";t=0;getFortune();}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Ignore Clicks Mid-Animation if(state==="opening")return;

Prevents restarting the animation or firing another API request while the cookie is already cracking open.

if(state==="opening")return;
If the crack animation is currently playing, ignore this click so it can finish uninterrupted.
state="opening";
Starts (or restarts) the crack animation by switching the state machine into the 'opening' phase.
t=0;
Resets the shared animation timer back to the start so the crack plays from the beginning.
getFortune();
Kicks off a new fetch() request to generate a fresh AI fortune for this crack.

getFortune()

getFortune() demonstrates how to combine an immediate visual placeholder with an asynchronous fetch() call, so the sketch stays responsive while it waits for a network response - a pattern used constantly in real-world web apps.

🔬 This loop creates 5 lucky numbers between 1 and 99. What happens if you change the loop count to i<3 and the range to random(1,1000)?

fortune="...";nums=[];for(let i=0;i<5;i++)nums.push(int(random(1,100)));
function getFortune(){if(!API_KEY){fortune="Set API_KEY in sketch.js";nums=[];return;}
fortune="...";nums=[];for(let i=0;i<5;i++)nums.push(int(random(1,100)));
fetch("https://api.openai.com/v1/chat/completions",{
method:"POST",
headers:{"Content-Type":"application/json","Authorization":"Bearer "+API_KEY},
body:JSON.stringify({model:"gpt-4o-mini",
messages:[
{role:"system",content:"You are a fortune cookie generating short, cryptic fortunes."},
{role:"user",content:"One mysterious fortune, max 15 words, no preface, no quotes."}],
max_tokens:40,temperature:0.9})})
.then(r=>r.json())
.then(d=>{fortune=d.choices&&d.choices[0]?.message?.content.trim()||fortune;})
.catch(e=>fortune="The spirits are silent. Try again.");}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Check For API Key if(!API_KEY){fortune="Set API_KEY in sketch.js";nums=[];return;}

Shows a setup reminder and stops early if no API key has been added, avoiding a failed network call.

for-loop Generate Lucky Numbers for(let i=0;i<5;i++)nums.push(int(random(1,100)));

Fills the nums array with five random whole numbers between 1 and 99 to display as lucky numbers.

calculation Handle API Response .then(r=>r.json()) .then(d=>{fortune=d.choices&&d.choices[0]?.message?.content.trim()||fortune;}) .catch(e=>fortune="The spirits are silent. Try again.");

Parses the JSON reply from OpenAI, pulls out the generated fortune text, and falls back to an error message if anything goes wrong.

if(!API_KEY){fortune="Set API_KEY in sketch.js";nums=[];return;}
If no API key has been entered at the top of the file, shows a helpful reminder instead of making a doomed network request.
fortune="...";nums=[];for(let i=0;i<5;i++)nums.push(int(random(1,100)));
Immediately shows a placeholder '...' fortune and generates five random lucky numbers, so the slip has something to display while waiting for the real fortune.
fetch("https://api.openai.com/v1/chat/completions",{...})
Sends a POST request to OpenAI's chat completion endpoint, asking the gpt-4o-mini model to write a fortune.
headers:{"Content-Type":"application/json","Authorization":"Bearer "+API_KEY}
Tells the server the request body is JSON and authenticates the request using the API key as a Bearer token.
messages:[{role:"system",content:"..."},{role:"user",content:"..."}]
Sets up the chat conversation: a system message defining the assistant's persona, and a user message asking for a short mysterious fortune.
max_tokens:40,temperature:0.9
Limits the reply length to about 40 tokens and sets temperature to 0.9 for more creative, varied wording.
.then(r=>r.json())
Waits for the network response and parses its body as JSON.
.then(d=>{fortune=d.choices&&d.choices[0]?.message?.content.trim()||fortune;})
Safely digs into the response object to extract the generated text, trims whitespace, and updates the global fortune variable; keeps the old fortune if anything is missing.
.catch(e=>fortune="The spirits are silent. Try again.");
Catches any network or parsing errors and shows a friendly in-theme error message instead of crashing.

windowResized()

windowResized() is a built-in p5.js callback that automatically runs whenever the browser window is resized, letting you keep responsive sketches that adapt to any screen size.

function windowResized(){resizeCanvas(windowWidth,windowHeight);}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth,windowHeight);
Resizes the canvas to match the browser window whenever it changes size, keeping the fortune cookie centered and full-screen.

📦 Key Variables

state string

Tracks which phase of the animation the cookie is in: 'closed', 'opening', or 'open', driving how draw functions behave.

let state="closed";
t number

Shared animation progress timer that goes from 0 to 1 while the cookie cracks open, used to compute rotation, position, and the slip's growth.

let t=0;
fortune string

Holds the current fortune text shown on the paper slip, starting as a placeholder and later replaced by the AI-generated response.

let fortune="Click the cookie";
nums array

Stores five randomly generated 'lucky numbers' displayed below the fortune text.

let nums=[];
cookieCol object

A p5.Color object storing the tan color used for the main cookie shape.

cookieCol=color(240,194,125);
shadowCol object

A p5.Color object storing a darker tan used to fake a shadow layer behind each cookie half.

shadowCol=color(205,150,80);
bgCol object

A p5.Color object storing the dark red background color painted every frame.

bgCol=color(40,0,0);
API_KEY string

Holds the OpenAI API key needed to authenticate fetch() requests; left empty by default and must be filled in to enable real fortunes.

const API_KEY="";

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG API_KEY constant

The OpenAI API key is meant to be pasted directly into client-side code, which exposes it to anyone who views the page source or opens dev tools, risking key theft and unexpected billing.

💡 Move the fetch() call to a small serverless function or backend proxy that holds the key server-side, and have the sketch call that proxy instead of OpenAI directly.

BUG mousePressed()

There is no way to return the cookie to the 'closed' state, so the 'Tap to open your fortune' prompt can never reappear once the cookie has been opened once.

💡 Add a reset condition (e.g. after a few seconds in the 'open' state) or a separate 'reset' button that sets state back to 'closed' and t back to 0.

STYLE entire sketch.js

The code is written in a very dense, minified style with multiple statements per line and single-letter variable names, which makes it harder to read and debug.

💡 Reformat with one statement per line and consistent spacing, and consider slightly more descriptive names (e.g. crackProgress instead of t) to improve readability for future edits.

FEATURE getFortune()

There's no visual loading indicator distinguishing the placeholder '...' fortune from a real error state, so users can't tell if the request is still in progress or has failed until text appears.

💡 Add a simple animated ellipsis or spinner while fortune === '...' and consider a subtle color change to indicate error states versus successful responses.

🔄 Code Flow

Code flow showing setup, draw, drawcookie, drawslip, mousepressed, getfortune, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> closedprompt[closed-prompt] closedprompt -->|not clicked| drawcookie[drawcookie] drawcookie --> animateopening[animate-opening] animateopening -->|t < 1| drawhalves[draw-halves] drawhalves --> drawcookie drawcookie -->|t >= 1| drawslip[drawslip] drawslip --> skipbeforereveal[skip-before-reveal] skipbeforereveal -->|slipT < 1| showfortunetext[show-fortune-text] showfortunetext -->|slip wide enough| showluckynumbers[show-lucky-numbers] showluckynumbers --> draw draw --> mousepressed[mousepressed] mousepressed -->|clicked| blockduringopening[block-during-opening] blockduringopening -->|not cracking| getfortune[getfortune] getfortune --> checkapikey[check-api-key] checkapikey -->|API key present| generateluckynumbers[generate-lucky-numbers] generateluckynumbers --> handleresponse[handle-response] handleresponse --> draw windowresized[windowresized] --> draw click setup href "#fn-setup" click draw href "#fn-draw" click closedprompt href "#sub-closed-prompt" click drawcookie href "#fn-drawcookie" click animateopening href "#sub-animate-opening" click drawhalves href "#sub-draw-halves" click drawslip href "#fn-drawslip" click skipbeforereveal href "#sub-skip-before-reveal" click showfortunetext href "#sub-show-fortune-text" click showluckynumbers href "#sub-show-lucky-numbers" click mousepressed href "#fn-mousepressed" click blockduringopening href "#sub-block-during-opening" click getfortune href "#fn-getfortune" click checkapikey href "#sub-check-api-key" click generateluckynumbers href "#sub-generate-lucky-numbers" click handleresponse href "#sub-handle-response" click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual experience does the AI Fortune Cookie sketch provide?

The sketch features a golden fortune cookie that visually cracks open to reveal a slip with a unique AI-generated fortune and lucky numbers.

How can users interact with the AI Fortune Cookie sketch?

Users can click on the golden fortune cookie to crack it open and receive a new, randomly generated fortune each time.

What creative coding concepts are demonstrated in this sketch?

This sketch showcases real-time API integration for generating content, as well as animation techniques for simulating the cookie's opening motion.

Preview

AI Fortune Cookie - Crack Open Your AI-Generated Destiny - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Fortune Cookie - Crack Open Your AI-Generated Destiny - Code flow showing setup, draw, drawcookie, drawslip, mousepressed, getfortune, windowresized
Code Flow Diagram