party portal

This sketch creates an interactive party portal with a shared live player counter across browser tabs, auto-triggered Party Mode when a threshold is reached, and animated confetti and emojis. It includes optional music playback, a hidden admin dashboard accessed via a secret key sequence, and persistent statistics using localStorage.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the party color pulse — The background cycles through neon colors at half-speed (0.5 Hz). Double the color frequency to make it flash more frantically.
  2. Make confetti fall slower — Confetti currently falls between 2.2 and 5.6 pixels per frame. Slow it down so it drifts more gracefully.
  3. Increase emoji size range — Emojis are 26-46 pixels. Make them bigger and more varied so they dominate the screen during parties.
  4. Lower the party threshold — Party auto-starts when 10 players join. Try 5 to make parties trigger more easily during testing.
  5. Make confetti more opaque — Confetti alpha is currently 220 (slightly transparent). Crank it to 255 for solid, vivid pieces.
  6. Extend the party duration — Parties last 30 seconds by default. Make them last a full minute so there's more time to celebrate.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a neon-styled party portal interface that demonstrates advanced p5.js techniques combined with web APIs. When enough players join (tracked in real-time across open browser tabs), the sketch triggers Party Mode: a spectacular display of animated confetti, floating emojis, and color-cycling background overlays powered by p5.js drawing functions. The frontend uses localStorage to synchronize shared state, p5.sound for optional music playback, and the Web Crypto API to secure an admin dashboard.

The code is organized into three layers: state management (loadState, updateSharedState, heartbeat), DOM interaction and admin login, and animated visuals (setup and draw). By studying it, you'll learn how to build collaborative experiences that sync across browser tabs using only localStorage, how to structure complex p5.js sketches with external state, and how animated classes (ConfettiPiece, FloatingEmoji) can drive particle effects without needing a physics library.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas with transparent fill, grabs references to all HTML elements, generates 180 confetti pieces and 40 emoji sprites, and registers this browser tab as an active player in localStorage. A heartbeat function runs every 5 seconds to keep the player alive, while a storage event listener watches for updates from other tabs.
  2. Each frame, draw() checks if Party Mode should end based on elapsed time, updates the countdown display, and—if partyActive is true—calls drawPartyVisuals() to render the celebration.
  3. drawPartyVisuals() uses sin() and lerp() to smoothly cycle background colors between neon pinks, blues, and purples at 0.5 Hz, creating a pulsing glow effect.
  4. Two particle classes drive the animation: ConfettiPiece instances fall from the top with rotation and drift, resetting at the bottom, while FloatingEmoji instances float upward with a sine-wave swing motion.
  5. When a player joins, registerPlayer() increments the player count in the shared state. If the player count equals the threshold, maybeAutoStartParty() automatically sets partyActive to true and partyEndTime to 30 seconds in the future.
  6. The hidden admin dashboard is revealed by typing R, T, Y within 3 seconds (tracked by handleSecretKey), then logging in with a SHA-256 hashed password. Admins can manually start/end parties, adjust the threshold, and reset statistics—all mutations sync across tabs instantly via localStorage.

🎓 Concepts You'll Learn

localStorage for cross-tab state syncParticle animation classesp5.sound music generationAnimated color interpolationHidden admin sequence detectionCryptographic password hashing

📝 Code Breakdown

setup()

setup() runs once at startup. Here it initializes the canvas, grabs DOM elements, loads or creates the shared state, and pre-creates all particle objects. The event listeners ensure the sketch stays in sync with other tabs and responds to user input (music, admin keys).

🔬 These two loops populate the animation arrays. What happens if you delete the emoji loop or change EMOJI_COUNT to 0? Will the party still work without emojis?

  // Create confetti & emoji entities
  for (let i = 0; i < CONFETTI_COUNT; i++) {
    confettiPieces.push(new ConfettiPiece());
  }
  for (let i = 0; i < EMOJI_COUNT; i++) {
    emojiSprites.push(new FloatingEmoji());
  }
function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(RGB);
  textFont(
    'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
  );
  textAlign(CENTER, CENTER);
  noStroke();

  ensureTabId();

  // Grab DOM references
  playerCountEl = document.getElementById("player-count");
  thresholdEl = document.getElementById("threshold-value");
  modeLabelEl = document.getElementById("mode-label");
  countdownContainerEl = document.getElementById("countdown-container");
  countdownSecondsEl = document.getElementById("countdown-seconds");
  musicToggleEl = document.getElementById("music-toggle");
  partyBannerEl = document.getElementById("party-banner");
  adminDashboardEl = document.getElementById("admin-dashboard");
  adminThresholdInputEl = document.getElementById("admin-threshold-input");
  adminApplyThresholdEl = document.getElementById("admin-apply-threshold");
  adminStartPartyEl = document.getElementById("admin-start-party");
  adminEndPartyEl = document.getElementById("admin-end-party");
  adminResetStatsEl = document.getElementById("admin-reset-stats");
  statTotalPartiesEl = document.getElementById("stat-total-parties");
  statTotalJoinsEl = document.getElementById("stat-total-joins");
  statLastPartyEl = document.getElementById("stat-last-party");
  adminModalEl = document.getElementById("admin-modal");
  adminPasswordInputEl = document.getElementById("admin-password-input");
  adminLoginEl = document.getElementById("admin-login-button");
  adminCancelEl = document.getElementById("admin-cancel-button");
  adminErrorEl = document.getElementById("admin-error");

  // Initialize state and register this player
  const initial = loadState();
  pruneStalePlayers(initial);
  localStorage.setItem(STATE_KEY, JSON.stringify(initial));
  applyStateToLocal(initial);
  registerPlayer();

  // Heartbeat & disconnect
  setInterval(heartbeat, HEARTBEAT_INTERVAL_MS);

  window.addEventListener("beforeunload", () => {
    try {
      leaveLobby();
    } catch (e) {
      // ignore
    }
  });

  // Listen for updates from other tabs
  window.addEventListener("storage", (e) => {
    if (e.key === STATE_KEY && e.newValue) {
      try {
        const state = JSON.parse(e.newValue);
        applyStateToLocal(state);
      } catch (err) {
        console.error("Party Portal: failed to parse storage state", err);
      }
    }
  });

  // Secret admin sequence
  document.addEventListener("keydown", handleSecretKey);

  // Music button
  if (musicToggleEl) {
    musicToggleEl.addEventListener("click", () => {
      if (musicOn) {
        stopMusic();
      } else {
        startMusic();
        musicToggleEl.textContent = "Stop Music";
      }
    });
  }

  // Admin login modal actions
  if (adminCancelEl) {
    adminCancelEl.addEventListener("click", () => {
      hideAdminLogin();
      adminErrorEl.textContent = "";
    });
  }

  if (adminLoginEl) {
    adminLoginEl.addEventListener("click", async () => {
      adminErrorEl.textContent = "";
      const pwd = adminPasswordInputEl.value || "";
      if (!pwd) {
        adminErrorEl.textContent = "Please enter a password.";
        return;
      }
      adminLoginEl.disabled = true;
      const ok = await verifyAdminPassword(pwd);
      adminLoginEl.disabled = false;
      if (ok) {
        hideAdminLogin();
        showAdminDashboard();
      } else {
        if (!adminErrorEl.textContent) {
          adminErrorEl.textContent = "Incorrect password.";
        }
      }
    });
  }

  if (adminPasswordInputEl) {
    adminPasswordInputEl.addEventListener("keyup", (e) => {
      if (e.key === "Enter") {
        adminLoginEl.click();
      }
    });
  }

  // Admin dashboard controls
  if (adminApplyThresholdEl) {
    adminApplyThresholdEl.addEventListener("click", () => {
      if (!isAdmin) return;
      const v = parseInt(adminThresholdInputEl.value, 10);
      if (!Number.isFinite(v) || v < 1 || v > 200) {
        alert("Please enter a threshold between 1 and 200.");
        return;
      }
      updateSharedState((state) => {
        state.threshold = v;
      });
    });
  }

  if (adminStartPartyEl) {
    adminStartPartyEl.addEventListener("click", () => {
      if (!isAdmin) return;
      startPartyManual();
    });
  }

  if (adminEndPartyEl) {
    adminEndPartyEl.addEventListener("click", () => {
      if (!isAdmin) return;
      endParty();
    });
  }

  if (adminResetStatsEl) {
    adminResetStatsEl.addEventListener("click", () => {
      if (!isAdmin) return;
      if (confirm("Reset statistics for everyone?")) {
        resetStats();
      }
    });
  }

  // Create confetti & emoji entities
  for (let i = 0; i < CONFETTI_COUNT; i++) {
    confettiPieces.push(new ConfettiPiece());
  }
  for (let i = 0; i < EMOJI_COUNT; i++) {
    emojiSprites.push(new FloatingEmoji());
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

loop DOM Element References playerCountEl = document.getElementById("player-count");

Caches references to all HTML elements so they can be updated when state changes

calculation State Initialization const initial = loadState();

Loads the shared state from localStorage or creates a default state if none exists

conditional Event Listener Registration window.addEventListener("storage", (e) => {

Listens for localStorage changes from other tabs and syncs the local state automatically

for-loop Particle Creation Loop for (let i = 0; i < CONFETTI_COUNT; i++) {

Pre-creates all confetti and emoji instances that will animate during Party Mode

createCanvas(windowWidth, windowHeight);
Creates a full-window canvas that fills the entire viewport and will be drawn on every frame
colorMode(RGB);
Sets color mode to RGB so fill() and other color functions use red, green, blue values (0-255)
textAlign(CENTER, CENTER);
Centers all text both horizontally and vertically so emoji position is at their center point
noStroke();
Disables outline drawing so all shapes (confetti, etc.) have solid fills only
ensureTabId();
Generates a unique ID for this browser tab using sessionStorage, used to track players across state updates
const initial = loadState();
Retrieves the shared state from localStorage; if corrupt or missing, returns a default empty state
pruneStalePlayers(initial);
Removes any player entries older than 15 seconds, cleaning up tabs that closed without properly unregistering
registerPlayer();
Adds this tab to the shared players list with a current timestamp, triggering auto-party logic if threshold is met
setInterval(heartbeat, HEARTBEAT_INTERVAL_MS);
Sets up a repeating heartbeat every 5 seconds to keep this tab's player entry alive and trigger party checks
window.addEventListener("beforeunload", () => {
When the user closes this tab, leaveLobby() removes it from the shared player list so the party auto-ends if threshold drops
window.addEventListener("storage", (e) => {
Listens for changes to localStorage from OTHER tabs and syncs the local state so the display updates instantly
document.addEventListener("keydown", handleSecretKey);
Watches for keyboard input to detect the hidden R, T, Y admin sequence
for (let i = 0; i < CONFETTI_COUNT; i++) {
Creates 180 confetti piece objects in the array before draw() runs, so they are ready to animate
for (let i = 0; i < EMOJI_COUNT; i++) {
Creates 40 emoji sprite objects in the array so they exist when Party Mode starts

draw()

draw() runs at 60 frames per second. Each frame, it clears the canvas, checks if the party should end, updates the countdown display, and—if Party Mode is active—renders the celebration. The conditional rendering (if partyActive) is an optimization: only animate expensive particles when there's a party happening.

🔬 This logic auto-ends the party after partyEndTime is reached. What happens if you comment out the endParty() line? Will the particles stop animating?

  // Manage party timing
  if (partyActive && partyEndTime && Date.now() >= partyEndTime) {
    endParty();
  }
function draw() {
  clear(); // transparent canvas so we see the CSS background

  // Manage party timing
  if (partyActive && partyEndTime && Date.now() >= partyEndTime) {
    endParty();
  }

  updateCountdownUi();

  if (partyActive) {
    drawPartyVisuals();
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Canvas Clear clear();

Erases the previous frame so only the current frame is visible, preventing motion trails

conditional Party Duration Check if (partyActive && partyEndTime && Date.now() >= partyEndTime) {

Checks if the party duration has expired and automatically ends the party if time is up

conditional Party Visuals Render if (partyActive) {

Only draws confetti and emojis when Party Mode is active, saving performance in Chill mode

clear();
Clears the canvas to transparent, revealing the CSS-styled background. Unlike background(), clear() doesn't draw a color—the HTML div layers show through.
if (partyActive && partyEndTime && Date.now() >= partyEndTime) {
Checks three things: is party active, is there an end time, and has the current time passed the end time? If all true, the party is over.
endParty();
Sets partyActive to false and removes the Party Mode visual styling, syncing the change across all tabs via localStorage
updateCountdownUi();
Updates the countdown display on the HTML page to show how many seconds are left in Party Mode
if (partyActive) {
Only runs the expensive animation loop if the party is actually active—avoids wasted CPU time in Chill mode
drawPartyVisuals();
Calls the function that animates confetti, emojis, and the color-cycling background glow

drawPartyVisuals()

drawPartyVisuals() is the heart of the celebration. It uses trigonometry (sin + lerp) to create a smooth color-cycling glow effect, then animates all 180 confetti pieces and 40 emojis. The push/pop pair isolates the background glow styling so it doesn't affect the particle drawing. This function only runs when partyActive is true.

🔬 These three lines animate different color channels independently. What happens if you change all three to use just `phase` instead of mixing phase, 1-phase, and 1-phase*0.5? Will the colors still pulse in different directions?

  const r = lerp(60, 255, phase);
  const g = lerp(40, 120, 1 - phase);
  const b = lerp(120, 255, 1 - phase * 0.5);

🔬 This loop updates and draws every confetti piece. What happens if you comment out the .draw() call so only the update() runs? Will the confetti still animate even though it's not being drawn?

  // Confetti
  for (let i = 0; i < confettiPieces.length; i++) {
    confettiPieces[i].update();
    confettiPieces[i].draw();
  }
function drawPartyVisuals() {
  const t = millis() / 1000;
  const phase = (sin(t * 2.0) + 1) / 2; // 0-1

  const r = lerp(60, 255, phase);
  const g = lerp(40, 120, 1 - phase);
  const b = lerp(120, 255, 1 - phase * 0.5);

  // Soft glow overlay
  push();
  noStroke();
  fill(r, g, b, 70);
  rect(0, 0, width, height);
  pop();

  // Confetti
  for (let i = 0; i < confettiPieces.length; i++) {
    confettiPieces[i].update();
    confettiPieces[i].draw();
  }

  // Emojis
  for (let i = 0; i < emojiSprites.length; i++) {
    emojiSprites[i].update();
    emojiSprites[i].draw();
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Animated Color Calculation const phase = (sin(t * 2.0) + 1) / 2; // 0-1

Uses sin() to create a smooth 0-1 oscillation that drives color transitions

calculation Background Glow Overlay rect(0, 0, width, height);

Draws a full-canvas rectangle with interpolated colors that smoothly pulse

for-loop Confetti Update and Draw for (let i = 0; i < confettiPieces.length; i++) {

Updates physics and redraws each confetti piece every frame

for-loop Emoji Update and Draw for (let i = 0; i < emojiSprites.length; i++) {

Updates position and redraws each emoji every frame with floating motion

const t = millis() / 1000;
Gets the current sketch time in seconds (millis() returns milliseconds, so divide by 1000). This drives the animation cycle.
const phase = (sin(t * 2.0) + 1) / 2; // 0-1
sin(t * 2.0) oscillates between -1 and 1 twice per second. Adding 1 shifts it to 0-2, then dividing by 2 gives 0-1 that smoothly waves up and down.
const r = lerp(60, 255, phase);
Interpolates the red channel between 60 (dark) and 255 (bright) based on phase, so it pulses from dim to bright and back.
const g = lerp(40, 120, 1 - phase);
Green uses 1 - phase, so it animates in the opposite direction—when red is bright, green is dim, creating color shift.
const b = lerp(120, 255, 1 - phase * 0.5);
Blue animates with a different curve (1 - phase * 0.5) to create an asymmetric color cycle that feels more dynamic.
fill(r, g, b, 70);
Sets the fill color to the interpolated RGB values with alpha 70 (semi-transparent) so the glow doesn't fully block the background.
rect(0, 0, width, height);
Draws a rectangle the size of the entire canvas with the glow color, layering on top of the CSS background.
push();
Saves the current drawing state (fill, stroke, etc.) so the rect() doesn't interfere with confetti and emoji drawing
pop();
Restores the previous drawing state so confetti and emojis draw with their own settings
confettiPieces[i].update();
Calls the update() method on each confetti piece, which moves it and checks for wrapping at screen edges
confettiPieces[i].draw();
Calls the draw() method on each confetti piece, rendering it at its current position with rotation
emojiSprites[i].update();
Moves the emoji up and sideways with a sine-wave wiggle
emojiSprites[i].draw();
Renders the emoji character at its current position

ConfettiPiece (class constructor)

ConfettiPiece is a class that encapsulates all the data and methods for a single confetti rectangle. The constructor initializes it by calling reset(). The reset() method handles both first spawning (initial = true, starts above the screen) and respawning when a piece falls off the bottom. Each piece has its own velocity, size, rotation, and color.

🔬 These lines randomize starting position and velocity. What happens if you remove the random() calls and use fixed values like this.speedY = 3? Will all confetti fall at the same speed?

    this.x = random(width);
    this.y = initial ? random(-height, 0) : random(-60, -10);
    this.size = random(6, 14);
    this.speedY = random(2.2, 5.6);
    this.speedX = random(-1.1, 1.1);
class ConfettiPiece {
  constructor() {
    this.reset(true);
  }

  reset(initial) {
    this.x = random(width);
    this.y = initial ? random(-height, 0) : random(-60, -10);
    this.size = random(6, 14);
    this.speedY = random(2.2, 5.6);
    this.speedX = random(-1.1, 1.1);
    this.rotation = random(TWO_PI);
    this.rotationSpeed = random(-0.12, 0.12);
    this.color = color(
      random(160, 255),
      random(100, 255),
      random(160, 255),
      220
    );
  }
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Position Randomization this.x = random(width);

Places the confetti at a random horizontal position across the canvas width

calculation Color Generation this.color = color(...);

Creates a random bright color with high RGB values so the confetti pops visually

class ConfettiPiece {
Defines a class (blueprint) for creating individual confetti particle objects with their own position, velocity, and appearance
constructor() {
The constructor runs automatically when a new ConfettiPiece() is created, initializing the object's state
this.reset(true);
Calls the reset() method with true to randomize all properties as if this is the first spawn—sets initial as true
reset(initial) {
A method that randomizes properties; the initial flag changes the starting Y position (off-screen above vs. just above screen)
this.x = random(width);
Random horizontal position anywhere across the canvas, so confetti spawns at different x coordinates
this.y = initial ? random(-height, 0) : random(-60, -10);
If first spawn, place above the screen; if respawning after falling, place just slightly above to maintain randomness
this.size = random(6, 14);
Random size between 6 and 14 pixels so not all confetti are uniform—creates visual variety
this.speedY = random(2.2, 5.6);
Downward velocity (pixels per frame)—higher values make it fall faster, the range adds randomness to timing
this.speedX = random(-1.1, 1.1);
Horizontal drift velocity—can be negative (left) or positive (right), adding sideways motion to the fall
this.rotation = random(TWO_PI);
Initial rotation in radians (full circle)—each confetti piece starts spinning at a different angle
this.rotationSpeed = random(-0.12, 0.12);
How fast the confetti spins each frame—negative rotates clockwise, positive counter-clockwise
this.color = color(...);
Generates a random bright color (R and B both 160-255, G 100-255) with alpha 220 so confetti is semi-transparent

ConfettiPiece.update()

update() is called every frame to move the confetti. It applies velocity to position, spins the piece, and checks for boundary conditions. When a piece falls off the bottom, it respawns at the top. When it drifts off the sides, it wraps around to the opposite side. This creates the illusion of infinite confetti falling.

  update() {
    this.x += this.speedX;
    this.y += this.speedY;
    this.rotation += this.rotationSpeed;

    if (this.y - this.size > height) {
      this.reset(false);
    }
    if (this.x < -20) this.x = width + 20;
    if (this.x > width + 20) this.x = -20;
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Position Update this.x += this.speedX;

Moves the confetti horizontally by adding velocity each frame

conditional Screen Wrapping if (this.x < -20) this.x = width + 20;

Teleports confetti to the other side when it goes too far left or right, creating seamless wrapping

conditional Bottom Respawn if (this.y - this.size > height) {

Detects when confetti has completely fallen off the bottom and respawns it at the top

this.x += this.speedX;
Moves confetti left or right by adding its horizontal velocity (can be negative, zero, or positive)
this.y += this.speedY;
Moves confetti down by adding its downward velocity—always positive, so it always falls
this.rotation += this.rotationSpeed;
Increments the rotation angle each frame so the confetti spins as it falls
if (this.y - this.size > height) {
Checks if the bottom edge of the confetti (y + size) has gone below the canvas height—if so, it's fully off-screen
this.reset(false);
Respawns the confetti at the top with a new random position, velocity, and color
if (this.x < -20) this.x = width + 20;
If confetti drifts too far left, wrap it to the right side, creating a seamless loop
if (this.x > width + 20) this.x = -20;
If confetti drifts too far right, wrap it to the left side

ConfettiPiece.draw()

draw() is called every frame to render the confetti rectangle. Using push/pop and translate/rotate, it positions the confetti at its absolute location, rotates it, and draws a rounded rectangle. The push/pop ensures the rotation doesn't affect other drawings in the same frame.

  draw() {
    push();
    translate(this.x, this.y);
    rotate(this.rotation);
    noStroke();
    fill(this.color);
    const w = this.size;
    const h = this.size * 0.45;
    rectMode(CENTER);
    rect(0, 0, w, h, 3);
    pop();
  }
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Transformation Stack push(); translate(this.x, this.y); rotate(this.rotation);

Uses push/pop and transforms to position and rotate the confetti before drawing it

calculation Confetti Rectangle rect(0, 0, w, h, 3);

Draws a rounded rectangle at the origin (which has been translated to the confetti's position)

push();
Saves the current transformation matrix and drawing settings so changes don't affect other drawings
translate(this.x, this.y);
Moves the coordinate system to the confetti's position so we draw at (0, 0) instead of absolute screen coordinates
rotate(this.rotation);
Rotates the coordinate system by the confetti's current rotation angle, so the rectangle draws at that angle
noStroke();
Disables outline so only the filled rectangle is visible
fill(this.color);
Sets the fill color to the confetti's random bright color
const w = this.size;
Width of the rectangle equals the confetti size
const h = this.size * 0.45;
Height is 45% of the size, creating an elongated rectangle instead of a square
rectMode(CENTER);
Makes the rectangle draw from its center point (0, 0) instead of the top-left, so rotation looks correct
rect(0, 0, w, h, 3);
Draws the rectangle with the last argument (3) rounding the corners for a softer appearance
pop();
Restores the previous transformation matrix and drawing state so subsequent draws aren't affected

FloatingEmoji (class constructor)

FloatingEmoji is similar to ConfettiPiece but simpler—it stores a random emoji character, position, size, and motion parameters. Each emoji has a unique alpha (opacity) to create visual depth. The swingPhase enables a smooth side-to-side motion via sine curve in update().

class FloatingEmoji {
  constructor() {
    this.reset(true);
  }

  reset(initial) {
    this.char = random(EMOJI_CHOICES);
    this.x = random(width);
    this.y = initial ? random(height) : height + random(20, 80);
    this.size = random(26, 46);
    this.speedY = random(-0.6, -1.8);
    this.driftX = random(-0.5, 0.5);
    this.alpha = random(180, 255);
    this.swingPhase = random(TWO_PI);
  }
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Emoji Character Selection this.char = random(EMOJI_CHOICES);

Picks a random emoji from the EMOJI_CHOICES array (🎉, 🎊, 🥳, etc.)

calculation Opacity Randomization this.alpha = random(180, 255);

Each emoji gets a different transparency (180-255) so they feel depth-varied

this.char = random(EMOJI_CHOICES);
Picks a random emoji from the array ['🎉', '🎊', '🥳', '✨', '💃', '🕺', '🎈', '🎶']
this.x = random(width);
Random horizontal position across the canvas
this.y = initial ? random(height) : height + random(20, 80);
If first spawn, start anywhere on canvas; if respawning, start below the screen
this.size = random(26, 46);
Emoji text size in pixels—varied so they appear in different scales
this.speedY = random(-0.6, -1.8);
Negative velocity so emojis float upward; varied speeds make some float faster than others
this.driftX = random(-0.5, 0.5);
Slight horizontal drift (left or right) to add wandering motion
this.alpha = random(180, 255);
Random opacity between 180 and 255—slightly transparent so overlapping emojis blend nicely
this.swingPhase = random(TWO_PI);
Random starting phase for the sine-wave swing motion, so emojis don't all swing in sync

FloatingEmoji.update()

update() moves the emoji upward and applies a sine-wave swing. The swingPhase accumulates each frame, driving a smooth left-right oscillation that makes the emojis feel alive and playful. When an emoji floats off the top, it respawns below the screen.

  update() {
    this.swingPhase += 0.04;
    this.x += this.driftX + Math.sin(this.swingPhase) * 0.2;
    this.y += this.speedY;

    if (this.y + this.size < -40) {
      this.reset(false);
    }
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Sine Wave Swing this.x += this.driftX + Math.sin(this.swingPhase) * 0.2;

Combines steady drift with a smooth sine-wave wiggle for natural floating motion

conditional Emoji Respawn if (this.y + this.size < -40) {

When emoji floats off the top, respawn it below the screen

this.swingPhase += 0.04;
Increments the phase each frame (0.04 radians) to drive the sine-wave motion smoothly
this.x += this.driftX + Math.sin(this.swingPhase) * 0.2;
Moves left/right by combining constant drift (driftX) with a sine oscillation (Math.sin) scaled by 0.2, creating a wavy float
this.y += this.speedY;
Moves upward by adding the negative speedY (upward motion)
if (this.y + this.size < -40) {
Checks if the bottom of the emoji text (y + size) has gone above the top of the screen by 40 pixels
this.reset(false);
Respawns the emoji with new properties below the screen

FloatingEmoji.draw()

draw() renders the emoji as p5.js text. Using translate and push/pop, it positions the emoji on screen and ensures its rotation and opacity are correct. The textAlign(CENTER, CENTER) ensures the emoji is visually centered at its x, y coordinates.

  draw() {
    push();
    translate(this.x, this.y);
    textAlign(CENTER, CENTER);
    textSize(this.size);
    fill(255, this.alpha);
    text(this.char, 0, 0);
    pop();
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Emoji Text Rendering text(this.char, 0, 0);

Renders the emoji character at the emoji's position with its opacity

push();
Saves the drawing state before transforming
translate(this.x, this.y);
Moves the coordinate system to the emoji's position
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically at the drawing position
textSize(this.size);
Sets the emoji's text size to its stored size value (26-46 pixels)
fill(255, this.alpha);
Sets the text color to white (255) with the emoji's random opacity (180-255)
text(this.char, 0, 0);
Draws the emoji character at (0, 0)—which is now the emoji's actual position due to translate()
pop();
Restores the previous drawing state

ensureTabId()

ensureTabId() generates or retrieves a unique identifier for this browser tab. It uses sessionStorage instead of localStorage so the ID is tied to the tab's session and doesn't leak to other tabs or persist after closing. This ID is used as a key in the shared player state so the sketch can track when this specific tab joins, leaves, or crashes.

function ensureTabId() {
  let existing = sessionStorage.getItem("partyPortalTabId");
  if (existing) {
    tabId = existing;
  } else {
    tabId = Math.random().toString(36).slice(2) + Date.now().toString(36);
    sessionStorage.setItem("partyPortalTabId", tabId);
  }
}
Line-by-line explanation (6 lines)
let existing = sessionStorage.getItem("partyPortalTabId");
Tries to retrieve an existing tab ID from sessionStorage (which persists only for the lifetime of the browser tab)
if (existing) {
If this tab already has an ID, reuse it
tabId = existing;
Assign the existing ID to the global tabId variable
} else {
If no ID exists, this must be the first time this tab is loading the sketch
tabId = Math.random().toString(36).slice(2) + Date.now().toString(36);
Generate a unique ID by combining a random string and the current timestamp in base-36 encoding—nearly guaranteed to be unique
sessionStorage.setItem("partyPortalTabId", tabId);
Store the new ID in sessionStorage so it persists if the page reloads (but only for this tab)

loadState()

loadState() is a defensive function that safely retrieves the shared state from localStorage. It handles three failure modes: missing data (first load), malformed JSON (corruption), and version mismatch. It always returns a valid state object, never throws, making it safe to call at any time.

function loadState() {
  const raw = localStorage.getItem(STATE_KEY);
  if (!raw) return defaultState();
  try {
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== "object" || parsed.version !== 1) {
      return defaultState();
    }
    return parsed;
  } catch (e) {
    console.warn("Party Portal: resetting corrupt state", e);
    return defaultState();
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional JSON Parse Error Handling try { ... } catch (e) { ... }

Safely parses JSON and returns a default state if corruption is detected

const raw = localStorage.getItem(STATE_KEY);
Retrieves the raw JSON string from localStorage using the STATE_KEY ('partyPortalState_v1')
if (!raw) return defaultState();
If no state exists in localStorage yet, return a fresh default state (first load)
const parsed = JSON.parse(raw);
Converts the JSON string into a JavaScript object
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) {
Validates the parsed object: is it truthy, is it an object, and does it have version 1? If any check fails, it's corrupt
return defaultState();
If validation fails, discard the corrupt state and return a fresh default
return parsed;
If all checks pass, return the parsed state object
} catch (e) { ... }
If JSON.parse() throws an exception (malformed JSON), catch it and return a default state

updateSharedState(mutator)

updateSharedState() follows a functional pattern: accept a mutator function, load state, let the mutator modify it, save it, and apply it locally. This pattern makes state changes predictable and ensures all three steps (save, prune, apply) always happen together. The storage event triggered by setItem() automatically syncs other tabs.

function updateSharedState(mutator) {
  let state = loadState();
  mutator(state);
  pruneStalePlayers(state);
  try {
    localStorage.setItem(STATE_KEY, JSON.stringify(state));
  } catch (e) {
    console.error("Party Portal: failed to save state", e);
  }
  applyStateToLocal(state);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Mutator Function Pattern mutator(state);

Calls the provided function to modify the state object in place

let state = loadState();
Loads the current shared state from localStorage
mutator(state);
Calls the provided function (mutator) which modifies the state object by reference—this is how changes are made
pruneStalePlayers(state);
Cleans up any players whose heartbeat has timed out before saving the state
localStorage.setItem(STATE_KEY, JSON.stringify(state));
Converts the modified state back to JSON and saves it to localStorage, triggering storage events in other tabs
applyStateToLocal(state);
Updates the local copy (currentPlayerCount, partyActive, etc.) and refreshes the DOM display

registerPlayer()

registerPlayer() is called once at setup() to announce that this tab is joining the lobby. It uses a mutator function to update the shared state atomically: increment totalJoins if new, add/update this tab's player entry, and check if the party should auto-start.

function registerPlayer() {
  const now = Date.now();
  updateSharedState((state) => {
    if (!state.players[tabId]) {
      state.stats.totalJoins = (state.stats.totalJoins || 0) + 1;
    }
    state.players[tabId] = now;
    maybeAutoStartParty(state, now);
  });
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional New Player Detection if (!state.players[tabId]) {

Checks if this tab is joining for the first time and increments totalJoins stat

const now = Date.now();
Gets the current time in milliseconds to record when this tab joined
updateSharedState((state) => {
Calls updateSharedState with a mutator function that will modify the shared state
if (!state.players[tabId]) {
Checks if this tab ID doesn't already exist in the players object—meaning this is a new join
state.stats.totalJoins = (state.stats.totalJoins || 0) + 1;
If new, increment the totalJoins counter (|| 0 ensures it defaults to 0 if undefined)
state.players[tabId] = now;
Adds (or updates) this tab's entry in the players map with the current timestamp
maybeAutoStartParty(state, now);
Checks if the new player count equals the threshold and auto-starts the party if it does

maybeAutoStartParty(state, now)

maybeAutoStartParty() implements the core automation: when the active player count reaches the configured threshold, the party automatically starts. It only starts once per threshold crossing (checks !isActive), and records statistics for later display. This function is called after every player join and heartbeat.

function maybeAutoStartParty(state, now) {
  pruneStalePlayers(state);
  const players = countPlayers(state);
  if (!state.party.isActive && players === state.threshold && players > 0) {
    state.party.isActive = true;
    state.party.endTime = now + PARTY_DURATION_MS;
    state.stats.totalParties = (state.stats.totalParties || 0) + 1;
    state.stats.lastPartyStart = now;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Threshold Trigger if (!state.party.isActive && players === state.threshold && players > 0) {

Triggers party start only when exact threshold is reached AND party isn't already active

pruneStalePlayers(state);
Removes stale player entries before counting, so the count is always current
const players = countPlayers(state);
Counts the active players in the state
if (!state.party.isActive && players === state.threshold && players > 0) {
Checks three conditions: party isn't already active, player count exactly matches threshold, and there's at least 1 player
state.party.isActive = true;
Flags the party as active
state.party.endTime = now + PARTY_DURATION_MS;
Sets the party end time to 30 seconds (PARTY_DURATION_MS) from now
state.stats.totalParties = (state.stats.totalParties || 0) + 1;
Increments the totalParties counter
state.stats.lastPartyStart = now;
Records the exact time the party started for the stats display

handleSecretKey(e)

handleSecretKey() detects the hidden R, T, Y admin sequence. It maintains a time-windowed buffer of recent key presses and checks if the last three keys match 'rty'. The window (3 seconds) forces the keys to be typed quickly. This is a lightweight easter-egg pattern for games and apps.

function handleSecretKey(e) {
  const now = Date.now();
  secretKeys.push({ key: e.key.toLowerCase(), time: now });
  secretKeys = secretKeys.filter((entry) => now - entry.time <= SECRET_WINDOW_MS);

  const sequence = secretKeys.map((entry) => entry.key).join("");
  if (sequence.endsWith("rty")) {
    showAdminLogin();
    secretKeys = [];
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Key Buffer and Windowing secretKeys = secretKeys.filter((entry) => now - entry.time <= SECRET_WINDOW_MS);

Keeps only recent keys (within 3 seconds) so the sequence must be typed quickly

conditional Sequence Match if (sequence.endsWith("rty")) {

Checks if the last three keys typed form 'rty' (R, T, Y)

const now = Date.now();
Gets the current time to track when this key was pressed
secretKeys.push({ key: e.key.toLowerCase(), time: now });
Adds the pressed key to the buffer with its timestamp (lowercased for case-insensitive matching)
secretKeys = secretKeys.filter((entry) => now - entry.time <= SECRET_WINDOW_MS);
Filters the buffer to keep only keys pressed within the last 3 seconds (SECRET_WINDOW_MS = 3000), removing old ones
const sequence = secretKeys.map((entry) => entry.key).join("");
Creates a string from the recent keys (e.g., 'xy' or 'rty')
if (sequence.endsWith("rty")) {
Checks if the sequence ends with 'rty'—it only needs to end with it, so 'xrty' would also match
showAdminLogin();
Shows the admin login modal if the sequence is detected
secretKeys = [];
Clears the buffer so the sequence can be entered again

startMusic()

startMusic() uses p5.sound's p5.Oscillator to generate a party theme by playing a sequence of four notes (A, C#, E, G) repeatedly. Each note has an attack (fast ramp up) and release (slow ramp down) to sound more musical. The userStartAudio() promise ensures the browser allows audio playback before the oscillator starts.

function startMusic() {
  if (musicOn) return;
  musicOn = true;

  if (!partyOsc) {
    partyOsc = new p5.Oscillator("square");
  }

  userStartAudio()
    .then(() => {
      partyOsc.start();
      partyOsc.amp(0.0);
      let noteIndex = 0;

      musicIntervalId = setInterval(() => {
        if (!musicOn) return;
        const freq = PARTY_NOTES[noteIndex % PARTY_NOTES.length];
        noteIndex++;
        partyOsc.freq(freq);
        partyOsc.amp(0.16, 0.05);

        setTimeout(() => {
          if (musicOn) {
            partyOsc.amp(0.04, 0.18);
          }
        }, NOTE_DURATION_MS * 0.6);
      }, NOTE_DURATION_MS);
    })
    .catch((err) => {
      console.error("Audio start failed", err);
      musicOn = false;
    });
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Oscillator Initialization if (!partyOsc) {

Creates a p5.Oscillator once and reuses it for all note playback

calculation User Audio Permission userStartAudio()

Requests browser permission to play audio (required by modern browsers)

calculation Note Sequencing setInterval(() => { ... }, NOTE_DURATION_MS);

Plays notes sequentially from PARTY_NOTES, cycling through frequencies

if (musicOn) return;
If music is already playing, exit early to avoid starting it twice
musicOn = true;
Sets the flag so we know music is playing
if (!partyOsc) {
Checks if the oscillator hasn't been created yet
partyOsc = new p5.Oscillator("square");
Creates a square-wave oscillator for a retro game-like sound
userStartAudio()
Asks the browser for permission to play audio (required by autoplay policies)
.then(() => {
If permission is granted, this callback runs
partyOsc.start();
Starts the oscillator (frequency and amplitude will be set by the interval)
partyOsc.amp(0.0);
Sets initial amplitude to 0 so the first note doesn't jump
const freq = PARTY_NOTES[noteIndex % PARTY_NOTES.length];
Gets the next frequency from PARTY_NOTES array, cycling back to start when reaching the end (% operator wraps)
partyOsc.freq(freq);
Sets the oscillator frequency to play that note
partyOsc.amp(0.16, 0.05);
Ramps the amplitude up to 0.16 over 0.05 seconds (attack) so the note starts smoothly
setTimeout(() => { ... }, NOTE_DURATION_MS * 0.6);
After 60% of the note duration (168ms of 280ms), schedule the release phase
partyOsc.amp(0.04, 0.18);
Ramps amplitude down to 0.04 over 0.18 seconds (release), creating a realistic note decay
.catch((err) => {
If the user denies audio permission or it fails, this callback runs
musicOn = false;
Resets the flag so the user can try again later

applyStateToLocal(state)

applyStateToLocal() synchronizes the shared state into both local JavaScript variables and DOM elements. This is the central hub where state changes become visible. It's called after every state update (loadState, storage events, mutations) and ensures that the UI always reflects the current truth.

function applyStateToLocal(state) {
  localState = state;

  currentPlayerCount = countPlayers(state);
  currentThreshold = state.threshold;
  partyActive = !!state.party.isActive;
  partyEndTime = state.party.endTime || 0;

  if (playerCountEl) playerCountEl.textContent = currentPlayerCount;
  if (thresholdEl) thresholdEl.textContent = currentThreshold;

  if (modeLabelEl) {
    if (partyActive) {
      modeLabelEl.textContent = "Party Mode";
      modeLabelEl.classList.add("party");
    } else {
      modeLabelEl.textContent = "Chill";
      modeLabelEl.classList.remove("party");
    }
  }

  if (adminThresholdInputEl) {
    adminThresholdInputEl.value = currentThreshold;
  }

  if (statTotalPartiesEl && state.stats) {
    statTotalPartiesEl.textContent = state.stats.totalParties || 0;
    statTotalJoinsEl.textContent = state.stats.totalJoins || 0;
    if (state.stats.lastPartyStart) {
      const d = new Date(state.stats.lastPartyStart);
      statLastPartyEl.textContent = d.toLocaleString();
    } else {
      statLastPartyEl.textContent = "—";
    }
  }

  updatePartyUi();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Local Variable Sync currentPlayerCount = countPlayers(state);

Updates local JavaScript variables that draw() uses without re-reading localStorage every frame

calculation DOM Update if (playerCountEl) playerCountEl.textContent = currentPlayerCount;

Updates HTML elements to display the new values

localState = state;
Caches the state object globally so other functions can reference it
currentPlayerCount = countPlayers(state);
Counts active players and stores in a local variable for quick access
currentThreshold = state.threshold;
Caches the threshold value
partyActive = !!state.party.isActive;
Converts the party.isActive to a boolean and caches it (the !! operator forces boolean conversion)
partyEndTime = state.party.endTime || 0;
Caches the party end timestamp (or 0 if not set)
if (playerCountEl) playerCountEl.textContent = currentPlayerCount;
If the element exists, update its text to show the player count
if (modeLabelEl) { ... modeLabelEl.classList.add("party"); ... }
Updates the mode label to 'Party Mode' or 'Chill' and adds/removes a CSS class to trigger styling
if (statTotalPartiesEl && state.stats) { ... }
Updates all stats display elements if they exist
const d = new Date(state.stats.lastPartyStart);
Converts the timestamp to a Date object for human-readable formatting
updatePartyUi();
Calls updatePartyUi() to handle CSS class changes and show/hide the party banner

📦 Key Variables

tabId string

Unique identifier for this browser tab, generated at startup and stored in sessionStorage. Used as a key in the shared players object.

let tabId;
localState object

Cached copy of the shared state object loaded from localStorage. Contains players map, threshold, party status, and stats.

let localState = null;
currentPlayerCount number

Number of active players (cached from localState) used by draw() and displayed in the header.

let currentPlayerCount = 0;
currentThreshold number

Party trigger threshold (cached from localState). When player count reaches this, party auto-starts.

let currentThreshold = 10;
partyActive boolean

Whether Party Mode is currently active. Controls whether drawPartyVisuals() runs each frame.

let partyActive = false;
partyEndTime number

Millisecond timestamp of when the party will end. Used by draw() to check if it's time to end the party.

let partyEndTime = 0;
secretKeys array of objects

Buffer of recent keypresses {key, time} used to detect the R-T-Y admin sequence within a 3-second window.

let secretKeys = [];
isAdmin boolean

Whether the current user is authenticated as admin. Controls visibility of the admin dashboard.

let isAdmin = false;
confettiPieces array of ConfettiPiece instances

Array of 180 confetti particles that animate and draw every frame when partyActive is true.

let confettiPieces = [];
emojiSprites array of FloatingEmoji instances

Array of 40 emoji particles that float and animate when partyActive is true.

let emojiSprites = [];
musicOn boolean

Whether the p5.sound oscillator is currently playing party music.

let musicOn = false;
partyOsc p5.Oscillator

p5.sound square-wave oscillator that plays the party theme. Created lazily in startMusic().

let partyOsc;
musicIntervalId number (interval ID)

ID of the setInterval that plays notes sequentially. Stored so it can be cleared by stopMusic().

let musicIntervalId = null;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG maybeAutoStartParty()

The party triggers when player count exactly equals threshold, but if two players join simultaneously (tab refresh, opening multiple windows), the count might jump from 8 to 10, skipping the exact threshold of 10 and never triggering.

💡 Use >= instead of === to trigger at or above threshold: if (!state.party.isActive && players >= state.threshold && players > 0)

PERFORMANCE draw() and drawPartyVisuals()

Every frame, the glow overlay rect is drawn even if the alpha is visually invisible during Chill mode. The 180 confetti pieces and 40 emojis have their own overhead.

💡 Pre-calculate color values and use a graphics buffer (createGraphics) to render the glow once per color cycle instead of every frame, or use CSS animations for the background instead of p5.js drawing.

STYLE ConfettiPiece and FloatingEmoji classes

Both classes have identical reset() and constructor() patterns but no shared base class, leading to duplicated code.

💡 Extract a common Particle base class with shared reset() logic and extend it for both ConfettiPiece and FloatingEmoji.

FEATURE Admin dashboard

The admin password hash is hardcoded as 'CHANGE_ME_TO_SHA256_HASH', and most deployments will leave it unchanged, leaving the admin panel accessible without a real password.

💡 Use environment variables or a config file to load the password hash at runtime, and add a warning to the console if the default hash is detected.

BUG handleSecretKey()

The secret sequence detection is vulnerable to backspacing: if you type 'RXRTY', the filter will remove old keys, and the sequence might match 'rty' accidentally.

💡 Instead of filtering old entries and checking endsWith(), track only the last 3 recent keys and match exactly.

PERFORMANCE storage event listener in setup()

Every time any tab updates localStorage, ALL tabs run applyStateToLocal() and DOM updates, even if unrelated keys changed.

💡 In the storage event listener, check if e.key === STATE_KEY before calling applyStateToLocal() to avoid unnecessary work.

🔄 Code Flow

Code flow showing setup, draw, drawpartyvisuals, confettipiececonstructor, confettipieceupdate, confettipiecedraw, floatingemojiconstructor, floatingemojiupdate, floatingemojidraw, ensureTabId, loadstate, updatesharedstate, registerplayer, maybeautostartparty, handlesecretkey, startmusic, applystatetoloca

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

graph TD start[Start] --> setup[setup] setup -->|Initialize| dom-grab[DOM Element References] setup -->|Load/Create State| state-init[State Initialization] setup -->|Register Events| event-listeners[Event Listener Registration] setup -->|Create Particles| particle-init[Particle Creation Loop] setup --> draw[draw loop] click setup href "#fn-setup" click dom-grab href "#sub-dom-grab" click state-init href "#sub-state-init" click event-listeners href "#sub-event-listeners" click particle-init href "#sub-particle-init" draw -->|Clear Canvas| canvas-clear[Canvas Clear] draw -->|Check Party Timeout| party-timeout[Party Duration Check] draw -->|Render Party| render-party[Party Visuals Render] draw -->|Update Local State| applystatetoloca[applyStateToLocal] draw -->|Update Shared State| updatesharedstate[updateSharedState] click draw href "#fn-draw" click canvas-clear href "#sub-canvas-clear" click party-timeout href "#sub-party-timeout" click render-party href "#sub-render-party" click applystatetoloca href "#fn-applystatetoloca" click updatesharedstate href "#fn-updatesharedstate" render-party -->|Draw Party Visuals| drawpartyvisuals[drawPartyVisuals] drawpartyvisuals -->|Animate Color| color-animation[Animated Color Calculation] drawpartyvisuals -->|Draw Background| background-glow[Background Glow Overlay] drawpartyvisuals -->|Update Confetti| confetti-loop[Confetti Update and Draw] drawpartyvisuals -->|Update Emojis| emoji-loop[Emoji Update and Draw] click drawpartyvisuals href "#fn-drawpartyvisuals" click color-animation href "#sub-color-animation" click background-glow href "#sub-background-glow" click confetti-loop href "#sub-confetti-loop" click emoji-loop href "#sub-emoji-loop" confetti-loop -->|Update Each Piece| confettipieceupdate[confettiPieceUpdate] confetti-loop -->|Draw Each Piece| confettipiecedraw[confettiPieceDraw] confetti-loop -->|Reset Position| reset-init[Position Randomization] confetti-loop -->|Generate Color| color-init[Color Generation] confetti-loop -->|Update Position| position-update[Position Update] confetti-loop -->|Check Boundaries| boundary-wrap[Screen Wrapping] confetti-loop -->|Respawn at Bottom| respawn-bottom[Bottom Respawn] confetti-loop -->|Transform for Drawing| transform-stack[Transformation Stack] confetti-loop -->|Draw Rectangle| rect-shape[Confetti Rectangle] click confettipieceupdate href "#fn-confettipieceupdate" click confettipiecedraw href "#fn-confettipiecedraw" click reset-init href "#sub-reset-init" click color-init href "#sub-color-init" click position-update href "#sub-position-update" click boundary-wrap href "#sub-boundary-wrap" click respawn-bottom href "#sub-respawn-bottom" click transform-stack href "#sub-transform-stack" click rect-shape href "#sub-rect-shape" emoji-loop -->|Update Each Emoji| floatingemojiupdate[floatingEmojiUpdate] emoji-loop -->|Draw Each Emoji| floatingemojidraw[floatingEmojiDraw] emoji-loop -->|Select Emoji| emoji-choice[Emoji Character Selection] emoji-loop -->|Randomize Opacity| opacity-randomize[Opacity Randomization] emoji-loop -->|Sine Wave Motion| swing-motion[Sine Wave Swing] emoji-loop -->|Respawn Emoji| respawn-emoji[Emoji Respawn] click floatingemojiupdate href "#fn-floatingemojiupdate" click floatingemojidraw href "#fn-floatingemojidraw" click emoji-choice href "#sub-emoji-choice" click opacity-randomize href "#sub-opacity-randomize" click swing-motion href "#sub-swing-motion" click respawn-emoji href "#sub-respawn-emoji" updatesharedstate -->|Mutate State| mutator-pattern[Mutator Function Pattern] updatesharedstate -->|Check New Player| new-player-check[New Player Detection] updatesharedstate -->|Check Threshold| threshold-check[Threshold Trigger] click mutator-pattern href "#sub-mutator-pattern" click new-player-check href "#sub-new-player-check" click threshold-check href "#sub-threshold-check" handleSecretKey -->|Buffer Keys| key-buffer[Key Buffer and Windowing] handleSecretKey -->|Check Sequence| sequence-detection[Sequence Match] click handleSecretKey href "#fn-handleSecretKey" click key-buffer href "#sub-key-buffer" click sequence-detection href "#sub-sequence-detection" startmusic -->|Initialize Audio| audio-init[Oscillator Initialization] startmusic -->|Request Audio| user-audio-gate[User Audio Permission] startmusic -->|Play Notes| note-loop[Note Sequencing] click startmusic href "#fn-startmusic" click audio-init href "#sub-audio-init" click user-audio-gate href "#sub-user-audio-gate" click note-loop href "#sub-note-loop" loadstate -->|Parse JSON| json-parse[JSON Parse Error Handling] click loadstate href "#fn-loadstate" click json-parse href "#sub-json-parse" ensureTabId -->|Generate ID| local-cache[Local Variable Sync] click ensureTabId href "#fn-ensureTabId" click local-cache href "#sub-local-cache"

❓ Frequently Asked Questions

What visual elements are featured in the party portal sketch?

The sketch showcases a glowing, neon interface with animated text, vibrant colors, and a dynamic party atmosphere enhanced by confetti and emojis.

How can users interact with the party portal and its features?

Users can join the lobby, toggle party music, and access a hidden admin dashboard by entering a secret key combination to manage the party settings.

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

This sketch demonstrates shared state management using localStorage, animated UI elements, and real-time synchronization across browser tabs.

Preview

party portal - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of party portal - Code flow showing setup, draw, drawpartyvisuals, confettipiececonstructor, confettipieceupdate, confettipiecedraw, floatingemojiconstructor, floatingemojiupdate, floatingemojidraw, ensureTabId, loadstate, updatesharedstate, registerplayer, maybeautostartparty, handlesecretkey, startmusic, applystatetoloca
Code Flow Diagram