3D Interactive Birthday Heart Tree Surprise

I turned HTML, CSS, and JavaScript into a special birthday surprise. An interactive 3D WebGL heart tree with pull-and-release slingshot, procedural branch growth, 600+ blooming heart petals, and music box melody.

Difficulty: Advanced Est. Time: 45 mins Vanilla JS (ES6+) Includes HTML, CSS, JS & README (.ZIP)
Live UI Runner

3D Interactive Birthday Heart Tree Surprise

live-sandbox://3d-birthday-surprise

1. Project Overview & Features

The 3D Interactive Birthday Heart Tree Surprise is an essential frontend project designed to help you bridge the gap between learning JavaScript syntax and building interactive, responsive web applications.

Key Features Implemented:

  • Interactive slingshot with elastic physics, drag-to-aim tension, and sound effects
  • Procedural 3D organic branch growth forming a romantic heart silhouette in real-time
  • 600+ vibrant 3D heart-shaped petals that bloom dynamically with depth & lighting
  • Drifting 3D falling petals simulation with rotational physics and soft wind turbulence
  • Cinematic romantic greeting card with live milestone clock and customizable dedication
  • Web Audio API acoustic music box synthesizer playing Happy Birthday
  • 3D mouse parallax and orbit controls allowing 360Β° inspection
  • Direct animated GIF recorder and complete multi-file ZIP export

2. Step-by-Step Code Walkthrough

A Step 1: Semantic HTML Structure

We structure the interface using clean, semantic HTML elements. This ensures maximum accessibility and provides intuitive hooks for CSS classes and JavaScript query selectors.

HTML (index.html)
<div class="bday-experience" id="bdayApp">
  <!-- Three.js 3D WebGL Viewport -->
  <canvas id="threeCanvas" class="three-canvas"></canvas>

  <!-- Slingshot Interactive 2D Overlay -->
  <canvas id="slingshotCanvas" class="slingshot-canvas"></canvas>

  <!-- Ambient Light Atmosphere -->
  <div class="ambient-glow"></div>

  <!-- Stage 1: Intro Callout & Slingshot Launcher -->
  <div class="stage-intro" id="stageIntro">
    <div class="intro-pill"><i class="fas fa-heart text-danger"></i> A Special 3D Surprise</div>
    <h1 class="intro-title">a little something, for you</h1>
    <div class="pulsing-heart-wrap">
      <div class="pulsing-heart">
        <i class="fas fa-heart"></i>
      </div>
    </div>
    
    <div class="slingshot-panel" id="slingshotPanel">
      <div class="slingshot-badge"><i class="fas fa-hand-rock"></i> PULL &amp; RELEASE</div>
      <p class="slingshot-sub">Drag the heart seed at the bottom-left and shoot to plant your tree!</p>
      <button type="button" class="btn-auto-shoot" id="btnAutoShoot">
        <i class="fas fa-magic me-1"></i> Quick Launch &amp; Bloom ✨
      </button>
    </div>
  </div>

  <!-- Stage 2: Cinematic Banner -->
  <div class="cinematic-banner hidden" id="cinematicBanner">
    <span class="banner-sub">and... make a wish...</span>
    <h2 class="banner-title">Happy Birthday</h2>
    <div class="gold-brush-stroke"></div>
  </div>

  <!-- Stage 3 & 4: Romantic Birthday Card & Live Milestone Clock -->
  <div class="bday-card-wrap hidden" id="bdayCard">
    <div class="card-glass">
      <button type="button" class="card-close-btn" id="btnCloseCard" title="Hide Birthday Card">&times;</button>
      <div class="card-top-tag">
        <i class="fas fa-sparkles text-warning me-1"></i> <span id="cardDedication">Made with Love &amp; Code πŸ’•</span>
      </div>
      <h2 class="card-greeting">Happy Birthday, <span class="recipient-name" id="cardRecipient">Sweetheart</span> ✨</h2>
      
      <div class="card-quote-box">
        <p class="card-quote">
          &ldquo;I turned HTML, CSS, and JavaScript into a special birthday surprise.<br>
          A special gift, made with time, effort, and a little bit of code. ✨&rdquo;
        </p>
      </div>

      <div class="card-message" id="cardCustomMsg">
        May every branch of your future blossom with peace, unbounded joy, and heartwarming love!
      </div>

      <!-- Live Milestone Counter -->
      <div class="milestone-box">
        <div class="milestone-header">Celebrating your journey of joy for:</div>
        <div class="milestone-grid">
          <div class="milestone-unit">
            <span class="m-val" id="cDays">8,421</span>
            <span class="m-label">Days</span>
          </div>
          <div class="milestone-unit">
            <span class="m-val" id="cHours">14</span>
            <span class="m-label">Hours</span>
          </div>
          <div class="milestone-unit">
            <span class="m-val" id="cMins">32</span>
            <span class="m-label">Mins</span>
          </div>
          <div class="milestone-unit">
            <span class="m-val" id="cSecs">45</span>
            <span class="m-label">Secs</span>
          </div>
        </div>
      </div>

      <div class="card-actions">
        <button type="button" class="card-btn btn-wish" id="btnOpenWish">
          <i class="fas fa-birthday-cake me-1"></i> Make a Wish πŸŽ‚
        </button>
        <button type="button" class="card-btn btn-customize" id="btnOpenCustom">
          <i class="fas fa-sliders-h me-1"></i> Personalize πŸ’Œ
        </button>
      </div>
    </div>
  </div>

  <!-- Subtle Bottom-Left Pill to Show Card When Hidden -->
  <button type="button" class="card-toggle-pill hidden" id="btnShowCardPill" title="Open Birthday Greeting Card">
    <i class="fas fa-envelope-open-text text-danger me-1"></i> <span>Greeting Card &amp; Milestone</span>
  </button>

  <!-- Floating Controls Dock -->
  <div class="controls-dock">
    <button type="button" class="dock-btn" id="btnToggleCard" title="Show/Hide Greeting Card">
      <i class="fas fa-envelope"></i>
    </button>
    <button type="button" class="dock-btn" id="btnAudioToggle" title="Toggle Music Box">
      <i class="fas fa-volume-up"></i>
    </button>
    <button type="button" class="dock-btn" id="btnReplay" title="Replay Slingshot & Blossom">
      <i class="fas fa-redo-alt"></i>
    </button>
    <button type="button" class="dock-btn" id="btnPalette" title="Cycle Color Themes">
      <i class="fas fa-palette"></i>
    </button>
    <button type="button" class="dock-btn" id="btnRecordGif" title="Record 3D Canvas as Animated GIF">
      <i class="fas fa-film"></i>
    </button>
  </div>

  <!-- 3D Parallax Hint -->
  <div class="orbit-hint" id="orbitHint">
    <i class="fas fa-cube me-1"></i> Move cursor / touch to rotate in 3D
  </div>

  <!-- Interactive Birthday Cake & Candle Modal -->
  <div class="modal-overlay hidden" id="wishModal">
    <div class="modal-card">
      <button type="button" class="btn-close-modal" id="btnCloseWish">&times;</button>
      <div class="cake-emoji">πŸŽ‚</div>
      <h3 class="modal-h3">Make a Birthday Wish</h3>
      <p class="modal-p">Close your eyes, make a heartfelt wish, then tap the candle flame to blow it out!</p>
      
      <div class="candle-stage" id="candleStage">
        <div class="candle-item">
          <div class="flame-glow" id="candleFlame"></div>
          <div class="wick"></div>
          <div class="wax"></div>
        </div>
      </div>
      <div class="tap-hint" id="tapFlameHint">✨ Tap the glowing flame to blow! ✨</div>

      <div class="wish-congrats hidden" id="wishCongrats">
        <i class="fas fa-sparkles fa-2x text-warning mb-2"></i>
        <h4>Happy Birthday! May all your wishes come true! πŸŽ‰βœ¨</h4>
        <p>A cascade of fireworks and blooming blessings for you!</p>
      </div>
    </div>
  </div>

  <!-- Personalize Modal -->
  <div class="modal-overlay hidden" id="customModal">
    <div class="modal-card">
      <button type="button" class="btn-close-modal" id="btnCloseCustom">&times;</button>
      <div class="cake-emoji">πŸ’Œ</div>
      <h3 class="modal-h3">Personalize Birthday Surprise</h3>
      
      <div class="form-row">
        <label>Recipient Name</label>
        <input type="text" id="inpName" value="Sweetheart" placeholder="e.g. Sarah, Emma, Alex">
      </div>

      <div class="form-row">
        <label>Birth Date (For Live Milestone Clock)</label>
        <input type="date" id="inpDate" value="2000-09-21">
      </div>

      <div class="form-row">
        <label>Blossom Color Palette</label>
        <select id="inpTheme">
          <option value="rose">🌸 Romantic Rose Blossom</option>
          <option value="sakura">✨ Sakura Gold &amp; Amber</option>
          <option value="sunset">πŸŒ… Coral Peach &amp; Ruby</option>
          <option value="twilight">πŸ’œ Violet Aurora &amp; Magenta</option>
        </select>
      </div>

      <div class="form-row">
        <label>Custom Dedication Message</label>
        <textarea id="inpMsg" rows="3">May every branch of your future blossom with peace, unbounded joy, and heartwarming love!</textarea>
      </div>

      <button type="button" class="btn-save-custom" id="btnSaveCustom">
        <i class="fas fa-check me-1"></i> Apply &amp; Re-Blossom
      </button>
    </div>
  </div>
</div>

B Step 2: Styling & Responsive CSS

Modern CSS flexbox and grid layouts are used to make the UI look polished, centered, and responsive across desktop, tablet, and mobile screens.

CSS (style.css)
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  user-select: none;
}
body, html {
  width: 100%;
  height: 100%;
  overflow: hidden;
  font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
  background: #fdf5f0;
}
.bday-experience {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: radial-gradient(circle at 50% 45%, #fff0f3 0%, #fae1dd 50%, #f7d6cd 100%);
}
.three-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}
.slingshot-canvas {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 320px;
  height: 300px;
  z-index: 10;
  pointer-events: auto;
  cursor: grab;
}
.slingshot-canvas:active {
  cursor: grabbing;
}
.ambient-glow {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 600px;
  height: 600px;
  background: radial-gradient(circle, rgba(255, 182, 193, 0.4) 0%, rgba(255, 228, 225, 0) 70%);
  border-radius: 50%;
  pointer-events: none;
  z-index: 2;
  filter: blur(40px);
  animation: pulseGlow 6s ease-in-out infinite alternate;
}
@keyframes pulseGlow {
  0% { transform: translate(-50%, -50%) scale(0.9); opacity: 0.5; }
  100% { transform: translate(-50%, -50%) scale(1.15); opacity: 0.85; }
}

/* Stage 1: Intro */
.stage-intro {
  position: absolute;
  top: 40px;
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
  z-index: 5;
  pointer-events: none;
  transition: opacity 0.8s ease, transform 0.8s ease;
}
.stage-intro.fade-out {
  opacity: 0;
  transform: translateX(-50%) translateY(-20px);
  pointer-events: none;
}
.intro-pill {
  display: inline-block;
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(8px);
  padding: 6px 16px;
  border-radius: 30px;
  font-size: 0.85rem;
  font-weight: 600;
  color: #e11d48;
  box-shadow: 0 4px 15px rgba(225, 29, 72, 0.12);
  margin-bottom: 12px;
}
.intro-title {
  font-family: 'Playfair Display', Georgia, serif;
  font-style: italic;
  font-size: 2.2rem;
  color: #4a2828;
  letter-spacing: 0.5px;
  text-shadow: 0 2px 10px rgba(255, 255, 255, 0.8);
}
.pulsing-heart-wrap {
  margin-top: 14px;
}
.pulsing-heart {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  background: radial-gradient(circle at 35% 35%, #ff758c 0%, #ff477e 100%);
  border-radius: 50%;
  color: #fff;
  font-size: 1.4rem;
  box-shadow: 0 0 25px rgba(255, 71, 126, 0.6), inset 0 2px 4px rgba(255, 255, 255, 0.6);
  animation: heartThrob 1.6s ease-in-out infinite;
}
@keyframes heartThrob {
  0%, 100% { transform: scale(1); box-shadow: 0 0 20px rgba(255, 71, 126, 0.5); }
  50% { transform: scale(1.18); box-shadow: 0 0 35px rgba(255, 71, 126, 0.85); }
}

.slingshot-panel {
  position: absolute;
  bottom: -220px;
  left: 20px;
  text-align: left;
  pointer-events: auto;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(10px);
  padding: 10px 16px;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.06);
  max-width: 280px;
}
.slingshot-badge {
  font-weight: 700;
  font-size: 0.85rem;
  color: #b91c1c;
  letter-spacing: 0.8px;
  margin-bottom: 4px;
}
.slingshot-sub {
  font-size: 0.78rem;
  color: #64748b;
  margin-bottom: 8px;
  line-height: 1.3;
}
.btn-auto-shoot {
  background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%);
  color: white;
  border: none;
  padding: 6px 14px;
  border-radius: 20px;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(225, 29, 72, 0.3);
  transition: all 0.2s;
}
.btn-auto-shoot:hover {
  transform: translateY(-1px);
  box-shadow: 0 6px 16px rgba(225, 29, 72, 0.45);
}

/* Stage 2: Cinematic Banner */
.cinematic-banner {
  position: absolute;
  top: 48%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1s ease, transform 1s cubic-bezier(0.16, 1, 0.3, 1);
}
.cinematic-banner.show {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
.banner-sub {
  font-family: 'Playfair Display', Georgia, serif;
  font-style: italic;
  font-size: 1.4rem;
  color: #be123c;
  display: block;
  margin-bottom: 6px;
}
.banner-title {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: 3.5rem;
  font-weight: 700;
  color: #881337;
  letter-spacing: 1px;
  text-shadow: 0 4px 20px rgba(255, 255, 255, 0.9);
}
.gold-brush-stroke {
  width: 220px;
  height: 4px;
  background: linear-gradient(90deg, transparent, #f59e0b, #fbbf24, transparent);
  margin: 10px auto 0;
  border-radius: 2px;
}

/* Stage 3 & 4: Romantic Birthday Card */
.bday-card-wrap {
  position: absolute;
  bottom: 30px;
  left: 30px;
  z-index: 15;
  max-width: 440px;
  width: calc(100% - 60px);
  pointer-events: auto;
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.bday-card-wrap.show {
  opacity: 1;
  transform: translateY(0);
}
.card-glass {
  position: relative;
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.95);
  border-radius: 24px;
  padding: 22px 24px;
  box-shadow: 0 20px 45px rgba(225, 29, 72, 0.14), 0 4px 12px rgba(0, 0, 0, 0.04);
}
.card-close-btn {
  position: absolute;
  top: 14px;
  right: 16px;
  background: rgba(241, 245, 249, 0.9);
  border: 1px solid rgba(203, 213, 225, 0.6);
  font-size: 1.15rem;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  cursor: pointer;
  color: #64748b;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  transition: all 0.2s;
}
.card-close-btn:hover {
  background: #fecdd3;
  color: #e11d48;
  transform: scale(1.1);
}
.card-toggle-pill {
  position: absolute;
  bottom: 24px;
  left: 24px;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.95);
  padding: 8px 18px;
  border-radius: 30px;
  font-size: 0.85rem;
  font-weight: 700;
  color: #475569;
  cursor: pointer;
  box-shadow: 0 8px 25px rgba(225, 29, 72, 0.15);
  z-index: 12;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  gap: 6px;
}
.card-toggle-pill:hover {
  background: #ffffff;
  color: #e11d48;
  transform: translateY(-2px);
  box-shadow: 0 12px 30px rgba(225, 29, 72, 0.25);
}
.card-top-tag {
  font-size: 0.78rem;
  font-weight: 700;
  color: #e11d48;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  margin-bottom: 6px;
}
.card-greeting {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: 1.85rem;
  font-weight: 700;
  color: #4c0519;
  line-height: 1.2;
  margin-bottom: 12px;
}
.recipient-name {
  color: #e11d48;
  border-bottom: 2px dashed #f43f5e;
}
.card-quote-box {
  background: rgba(255, 241, 242, 0.75);
  border-left: 3px solid #f43f5e;
  border-radius: 0 12px 12px 0;
  padding: 10px 14px;
  margin-bottom: 12px;
}
.card-quote {
  font-size: 0.88rem;
  font-style: italic;
  color: #881337;
  line-height: 1.5;
}
.card-message {
  font-size: 0.86rem;
  color: #475569;
  line-height: 1.5;
  margin-bottom: 14px;
}

/* Milestone Grid */
.milestone-box {
  background: rgba(255, 255, 255, 0.9);
  border-radius: 14px;
  padding: 10px 12px;
  margin-bottom: 16px;
  border: 1px solid #ffe4e6;
}
.milestone-header {
  font-size: 0.72rem;
  font-weight: 600;
  color: #9f1239;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 6px;
  text-align: center;
}
.milestone-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
  text-align: center;
}
.milestone-unit {
  background: #fff1f2;
  border-radius: 8px;
  padding: 6px 2px;
}
.m-val {
  display: block;
  font-size: 1.1rem;
  font-weight: 800;
  color: #be123c;
  font-family: monospace;
}
.m-label {
  display: block;
  font-size: 0.65rem;
  font-weight: 600;
  color: #9f1239;
  text-transform: uppercase;
}

.card-actions {
  display: flex;
  gap: 10px;
}
.card-btn {
  flex: 1;
  padding: 10px 14px;
  border-radius: 14px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  border: none;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.btn-wish {
  background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%);
  color: white;
  box-shadow: 0 4px 14px rgba(225, 29, 72, 0.35);
}
.btn-wish:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 18px rgba(225, 29, 72, 0.45);
}
.btn-customize {
  background: #f1f5f9;
  color: #334155;
  border: 1px solid #cbd5e1;
}
.btn-customize:hover {
  background: #e2e8f0;
}

/* Floating Dock */
.controls-dock {
  position: absolute;
  top: 20px;
  right: 20px;
  display: flex;
  gap: 8px;
  z-index: 20;
}
.dock-btn {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.9);
  color: #475569;
  font-size: 0.95rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
  transition: all 0.2s;
}
.dock-btn:hover {
  background: #fff;
  color: #e11d48;
  transform: scale(1.08);
  box-shadow: 0 6px 20px rgba(225, 29, 72, 0.25);
}
.dock-btn.active {
  background: #e11d48;
  color: #fff;
}

/* Orbit Hint */
.orbit-hint {
  position: absolute;
  bottom: 20px;
  right: 20px;
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(6px);
  color: #fff;
  padding: 6px 14px;
  border-radius: 20px;
  font-size: 0.75rem;
  font-weight: 500;
  pointer-events: none;
  z-index: 5;
  opacity: 0.8;
}

/* Modals */
.modal-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(15, 23, 42, 0.6);
  backdrop-filter: blur(10px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: 20px;
  transition: opacity 0.3s ease;
}
.modal-overlay.hidden {
  display: none;
}
.modal-card {
  background: #ffffff;
  border-radius: 24px;
  padding: 30px;
  max-width: 440px;
  width: 100%;
  position: relative;
  text-align: center;
  box-shadow: 0 25px 60px rgba(0, 0, 0, 0.3);
  animation: modalPop 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes modalPop {
  0% { transform: scale(0.9); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
.btn-close-modal {
  position: absolute;
  top: 16px;
  right: 18px;
  background: #f1f5f9;
  border: none;
  font-size: 1.2rem;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  color: #64748b;
  display: flex;
  align-items: center;
  justify-content: center;
}
.btn-close-modal:hover { background: #e2e8f0; color: #0f172a; }
.cake-emoji { font-size: 3rem; margin-bottom: 8px; }
.modal-h3 { font-family: 'Playfair Display', Georgia, serif; font-size: 1.6rem; color: #1e293b; margin-bottom: 6px; }
.modal-p { font-size: 0.85rem; color: #64748b; margin-bottom: 16px; line-height: 1.4; }

/* Interactive Candle */
.candle-stage {
  height: 120px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 10px;
}
.candle-item {
  position: relative;
  width: 24px;
  height: 70px;
  background: linear-gradient(to right, #fb7185, #f43f5e);
  border-radius: 6px;
  box-shadow: 0 4px 10px rgba(244, 63, 94, 0.3);
}
.wick {
  position: absolute;
  top: -10px;
  left: 50%;
  transform: translateX(-50%);
  width: 3px;
  height: 10px;
  background: #334155;
  border-radius: 2px;
}
.flame-glow {
  position: absolute;
  top: -32px;
  left: 50%;
  transform: translateX(-50%);
  width: 18px;
  height: 26px;
  background: radial-gradient(ellipse at 50% 80%, #fff 0%, #fef08a 40%, #f97316 80%, transparent 100%);
  border-radius: 50% 50% 20% 20%;
  box-shadow: 0 0 20px #f97316, 0 0 35px #fbbf24;
  cursor: pointer;
  animation: flicker 0.15s ease-in-out infinite alternate;
}
.flame-glow.blown-out {
  display: none;
}
@keyframes flicker {
  0% { transform: translateX(-50%) scale(1) rotate(-1deg); }
  100% { transform: translateX(-50%) scale(1.08) rotate(1.5deg); }
}
.tap-hint { font-size: 0.8rem; font-weight: 600; color: #f43f5e; margin-bottom: 10px; }
.wish-congrats {
  background: #fff1f2;
  border: 1px solid #fecdd3;
  border-radius: 14px;
  padding: 16px;
  margin-top: 10px;
}
.wish-congrats h4 { color: #9f1239; font-size: 1.1rem; margin-bottom: 4px; }
.wish-congrats p { color: #be123c; font-size: 0.82rem; margin: 0; }

/* Custom Form */
.form-row { text-align: left; margin-bottom: 12px; }
.form-row label { display: block; font-size: 0.78rem; font-weight: 600; color: #334155; margin-bottom: 4px; }
.form-row input, .form-row select, .form-row textarea {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #cbd5e1;
  border-radius: 10px;
  font-size: 0.85rem;
  outline: none;
  font-family: inherit;
  transition: border-color 0.2s;
}
.form-row input:focus, .form-row select:focus, .form-row textarea:focus {
  border-color: #f43f5e;
}
.btn-save-custom {
  width: 100%;
  background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%);
  color: white;
  border: none;
  padding: 10px;
  border-radius: 12px;
  font-size: 0.9rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(225, 29, 72, 0.3);
}

.hidden { display: none !important; }

@media (max-width: 768px) {
  .intro-title { font-size: 1.6rem; }
  .bday-card-wrap { bottom: 15px; left: 15px; width: calc(100% - 30px); }
  .banner-title { font-size: 2.4rem; }
  .slingshot-panel { bottom: -200px; max-width: 240px; }
}

C Step 3: JavaScript Logic & Event Handling

Here is the complete JavaScript code handling the state, event listeners, mathematical logic, and dynamic DOM rendering:

JavaScript (script.js)
(() => {
  // DOM Elements
  const threeCanvas = document.getElementById('threeCanvas');
  const slingshotCanvas = document.getElementById('slingshotCanvas');
  const stageIntro = document.getElementById('stageIntro');
  const cinematicBanner = document.getElementById('cinematicBanner');
  const bdayCard = document.getElementById('bdayCard');
  const btnCloseCard = document.getElementById('btnCloseCard');
  const btnToggleCard = document.getElementById('btnToggleCard');
  const btnShowCardPill = document.getElementById('btnShowCardPill');
  const btnAutoShoot = document.getElementById('btnAutoShoot');
  const btnAudioToggle = document.getElementById('btnAudioToggle');
  const btnReplay = document.getElementById('btnReplay');
  const btnPalette = document.getElementById('btnPalette');
  const btnRecordGif = document.getElementById('btnRecordGif');
  const btnOpenWish = document.getElementById('btnOpenWish');
  const btnOpenCustom = document.getElementById('btnOpenCustom');
  const wishModal = document.getElementById('wishModal');
  const btnCloseWish = document.getElementById('btnCloseWish');
  const candleFlame = document.getElementById('candleFlame');
  const tapFlameHint = document.getElementById('tapFlameHint');
  const wishCongrats = document.getElementById('wishCongrats');
  const customModal = document.getElementById('customModal');
  const btnCloseCustom = document.getElementById('btnCloseCustom');
  const btnSaveCustom = document.getElementById('btnSaveCustom');

  // Milestone Counter Elements
  const cDays = document.getElementById('cDays');
  const cHours = document.getElementById('cHours');
  const cMins = document.getElementById('cMins');
  const cSecs = document.getElementById('cSecs');

  // State
  let recipientName = 'Sweetheart';
  let birthDate = new Date('2000-09-21T00:00:00');
  let customMsg = 'May every branch of your future blossom with peace, unbounded joy, and heartwarming love!';
  let themeIndex = 0;
  const themes = [
    { name: 'rose', colors: [0xff6b8b, 0xff8da1, 0xffa8a8, 0xffd166, 0xf78fb3, 0xff477e] },
    { name: 'sakura', colors: [0xff9ebb, 0xffc2d1, 0xffe5ec, 0xfcd34d, 0xf59e0b, 0xffb703] },
    { name: 'sunset', colors: [0xfb7185, 0xf43f5e, 0xf97316, 0xfbbf24, 0xf87171, 0xef4444] },
    { name: 'twilight', colors: [0xc084fc, 0xa855f7, 0xe879f9, 0xf472b6, 0xec4899, 0x7c3aed] }
  ];

  let isPlanted = false;
  let treeProgress = 0.0; // 0 to 1
  let targetProgress = 0.0;
  let audioEnabled = false;
  let audioCtx = null;

  // --- 1. Three.js 3D WebGL Setup ---
  let scene, camera, renderer;
  let treeGroup = null;
  let branchSegments = [];
  let petalMeshes = [];
  let fallingPetals = [];
  let bgParticles = [];

  let mouseX = 0, mouseY = 0;
  let targetCamX = 0, targetCamY = 3.5;
  let isDraggingOrbit = false;
  let prevMouseX = 0, prevMouseY = 0;
  let orbitAngleX = 0, orbitAngleY = 0;

  function initThree() {
    scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0xfbe7df, 0.015);

    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 3.5, 26);

    renderer = new THREE.WebGLRenderer({
      canvas: threeCanvas,
      alpha: true,
      antialias: true,
      preserveDrawingBuffer: true
    });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.1;

    // Lighting
    const ambientLight = new THREE.AmbientLight(0xfff5f5, 1.2);
    scene.add(ambientLight);

    const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
    dirLight.position.set(10, 20, 15);
    scene.add(dirLight);

    const pointLight = new THREE.PointLight(0xff477e, 2.5, 45);
    pointLight.position.set(0, 6, 4);
    scene.add(pointLight);

    // Floating Background Ambient Dust Motes
    const dustGeo = new THREE.BufferGeometry();
    const dustCount = 140;
    const dustPositions = new Float32Array(dustCount * 3);
    for (let i = 0; i < dustCount * 3; i += 3) {
      dustPositions[i] = (Math.random() - 0.5) * 40;
      dustPositions[i + 1] = Math.random() * 25 - 5;
      dustPositions[i + 2] = (Math.random() - 0.5) * 30;
    }
    dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPositions, 3));
    const dustMat = new THREE.PointsMaterial({
      color: 0xffd1dc,
      size: 0.35,
      transparent: true,
      opacity: 0.6,
      blending: THREE.AdditiveBlending
    });
    const dustPoints = new THREE.Points(dustGeo, dustMat);
    scene.add(dustPoints);
    bgParticles.push(dustPoints);

    // Initial Tree Construction
    buildProceduralTree();
    initFallingPetals();

    window.addEventListener('resize', onWindowResize);
    threeCanvas.addEventListener('mousedown', onPointerDown);
    window.addEventListener('mousemove', onPointerMove);
    window.addEventListener('mouseup', onPointerUp);

    threeCanvas.addEventListener('touchstart', onTouchStart, { passive: false });
    window.addEventListener('touchmove', onTouchMove, { passive: false });
    window.addEventListener('touchend', onTouchEnd);
  }

  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    initSlingshotCanvas();
  }

  // --- Heart Shape Geometry Helper ---
  function createHeartShape() {
    const shape = new THREE.Shape();
    const x = 0, y = 0;
    shape.moveTo(x, y + 0.3);
    shape.bezierCurveTo(x, y + 0.6, x - 0.5, y + 0.85, x - 0.7, y + 0.55);
    shape.bezierCurveTo(x - 0.9, y + 0.25, x - 0.6, y - 0.25, x, y - 0.75);
    shape.bezierCurveTo(x + 0.6, y - 0.25, x + 0.9, y + 0.25, x + 0.7, y + 0.55);
    shape.bezierCurveTo(x + 0.5, y + 0.85, x, y + 0.6, x, y + 0.3);
    return shape;
  }

  const heartGeo = new THREE.ShapeGeometry(createHeartShape(), 12);

  // --- 2. Procedural 3D Heart Tree ---
  function buildProceduralTree() {
    if (treeGroup) scene.remove(treeGroup);
    treeGroup = new THREE.Group();
    branchSegments = [];
    petalMeshes = [];

    const barkMat = new THREE.MeshStandardMaterial({
      color: 0x3d231e,
      roughness: 0.85,
      metalness: 0.1
    });

    // 1. Trunk
    const trunkCurve = new THREE.CatmullRomCurve3([
      new THREE.Vector3(0, -6, 0),
      new THREE.Vector3(0.1, -2, 0),
      new THREE.Vector3(-0.1, 1.5, 0),
      new THREE.Vector3(0, 3.5, 0)
    ]);
    const trunkGeo = new THREE.TubeGeometry(trunkCurve, 20, 0.45, 8, false);
    const trunkMesh = new THREE.Mesh(trunkGeo, barkMat);
    treeGroup.add(trunkMesh);
    branchSegments.push({ mesh: trunkMesh, minP: 0.0, maxP: 0.3 });

    // 2. Heart-Shaped Branch Array (Left & Right Main Arcs)
    const branchConfigs = [
      // Left Heart Arc
      {
        points: [
          new THREE.Vector3(0, 3.5, 0),
          new THREE.Vector3(-1.8, 5.2, 0.4),
          new THREE.Vector3(-4.2, 7.5, 0.6),
          new THREE.Vector3(-5.5, 9.8, 0.2),
          new THREE.Vector3(-4.5, 12.2, -0.2),
          new THREE.Vector3(-2.2, 12.8, -0.4),
          new THREE.Vector3(0, 10.5, 0)
        ],
        radius: 0.3,
        minP: 0.15, maxP: 0.7
      },
      // Right Heart Arc
      {
        points: [
          new THREE.Vector3(0, 3.5, 0),
          new THREE.Vector3(1.8, 5.2, -0.4),
          new THREE.Vector3(4.2, 7.5, -0.6),
          new THREE.Vector3(5.5, 9.8, -0.2),
          new THREE.Vector3(4.5, 12.2, 0.2),
          new THREE.Vector3(2.2, 12.8, 0.4),
          new THREE.Vector3(0, 10.5, 0)
        ],
        radius: 0.3,
        minP: 0.15, maxP: 0.7
      },
      // Inner Sub-branches
      {
        points: [
          new THREE.Vector3(-1.8, 5.2, 0.4),
          new THREE.Vector3(-2.8, 8.0, 1.2),
          new THREE.Vector3(-1.5, 9.5, 0.8)
        ],
        radius: 0.18,
        minP: 0.35, maxP: 0.8
      },
      {
        points: [
          new THREE.Vector3(1.8, 5.2, -0.4),
          new THREE.Vector3(2.8, 8.0, -1.2),
          new THREE.Vector3(1.5, 9.5, -0.8)
        ],
        radius: 0.18,
        minP: 0.35, maxP: 0.8
      },
      {
        points: [
          new THREE.Vector3(-4.2, 7.5, 0.6),
          new THREE.Vector3(-5.8, 8.5, 0.8),
          new THREE.Vector3(-6.2, 10.5, 0.3)
        ],
        radius: 0.16,
        minP: 0.4, maxP: 0.85
      },
      {
        points: [
          new THREE.Vector3(4.2, 7.5, -0.6),
          new THREE.Vector3(5.8, 8.5, -0.8),
          new THREE.Vector3(6.2, 10.5, -0.3)
        ],
        radius: 0.16,
        minP: 0.4, maxP: 0.85
      }
    ];

    branchConfigs.forEach(bc => {
      const curve = new THREE.CatmullRomCurve3(bc.points);
      const geo = new THREE.TubeGeometry(curve, 18, bc.radius, 6, false);
      const mesh = new THREE.Mesh(geo, barkMat);
      treeGroup.add(mesh);
      branchSegments.push({ mesh, minP: bc.minP, maxP: bc.maxP });
    });

    // 3. 600+ Blooming 3D Heart Petals
    const curColors = themes[themeIndex].colors;
    const petalCount = 680;

    for (let i = 0; i < petalCount; i++) {
      // Parametric Heart Volume Distribution
      const t = Math.PI * 2 * Math.random();
      // Heart formula: x = 16 sin^3(t), y = 13 cos(t) - 5 cos(2t) - 2 cos(3t) - cos(4t)
      const hScale = 0.36 + Math.random() * 0.12;
      const hx = 16 * Math.pow(Math.sin(t), 3) * hScale * (0.65 + Math.random() * 0.38);
      const hy = (13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t)) * hScale * (0.65 + Math.random() * 0.38) + 9.6;
      const hz = (Math.random() - 0.5) * 4.5;

      const col = curColors[Math.floor(Math.random() * curColors.length)];
      const pMat = new THREE.MeshStandardMaterial({
        color: col,
        roughness: 0.4,
        metalness: 0.15,
        side: THREE.DoubleSide
      });

      const pMesh = new THREE.Mesh(heartGeo, pMat);
      const baseScale = 0.35 + Math.random() * 0.32;
      pMesh.position.set(hx, hy, hz);
      pMesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
      pMesh.scale.set(0.001, 0.001, 0.001); // starts invisible

      treeGroup.add(pMesh);
      petalMeshes.push({
        mesh: pMesh,
        targetScale: baseScale,
        bloomThreshold: 0.35 + Math.random() * 0.55,
        rotSpeed: (Math.random() - 0.5) * 0.02
      });
    }

    treeGroup.scale.set(0.001, 0.001, 0.001);
    scene.add(treeGroup);
  }

  // --- 3. Drifting 3D Falling Petals ---
  function initFallingPetals() {
    fallingPetals.forEach(p => scene.remove(p.mesh));
    fallingPetals = [];

    const curColors = themes[themeIndex].colors;
    const count = 120;

    for (let i = 0; i < count; i++) {
      const col = curColors[Math.floor(Math.random() * curColors.length)];
      const mat = new THREE.MeshStandardMaterial({
        color: col,
        roughness: 0.35,
        side: THREE.DoubleSide,
        transparent: true,
        opacity: 0.85
      });
      const mesh = new THREE.Mesh(heartGeo, mat);
      const s = 0.22 + Math.random() * 0.24;
      mesh.scale.set(s, s, s);

      mesh.position.set(
        (Math.random() - 0.5) * 26,
        15 + Math.random() * 12,
        (Math.random() - 0.5) * 16
      );
      mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);

      scene.add(mesh);
      fallingPetals.push({
        mesh,
        vy: 0.025 + Math.random() * 0.035,
        vx: (Math.random() - 0.5) * 0.015,
        vz: (Math.random() - 0.5) * 0.015,
        rotX: (Math.random() - 0.5) * 0.03,
        rotY: (Math.random() - 0.5) * 0.03,
        rotZ: (Math.random() - 0.5) * 0.02,
        swayOffset: Math.random() * 10
      });
    }
  }

  // --- 4. Interactive 2D Slingshot Engine ---
  let sCtx = null;
  let sWidth = 320, sHeight = 300;
  const slingAnchorLeft = { x: 80, y: 190 };
  const slingAnchorRight = { x: 170, y: 180 };
  const seedOrigin = { x: 125, y: 185 };
  let seedCurrent = { x: 125, y: 185 };
  let isDraggingSeed = false;
  let seedProjectile = null;

  function initSlingshotCanvas() {
    slingshotCanvas.width = 320;
    slingshotCanvas.height = 300;
    sCtx = slingshotCanvas.getContext('2d');
    sWidth = slingshotCanvas.width;
    sHeight = slingshotCanvas.height;
  }

  function renderSlingshot() {
    if (!sCtx) return;
    sCtx.clearRect(0, 0, sWidth, sHeight);

    if (isPlanted && !seedProjectile) return;

    // Draw Slingshot Wooden Fork
    sCtx.lineWidth = 14;
    sCtx.lineCap = 'round';
    sCtx.lineJoin = 'round';
    sCtx.strokeStyle = '#5c2c16';

    // Base Post
    sCtx.beginPath();
    sCtx.moveTo(125, 290);
    sCtx.lineTo(125, 220);
    sCtx.stroke();

    // Left Prong
    sCtx.beginPath();
    sCtx.moveTo(125, 220);
    sCtx.quadraticCurveTo(95, 210, slingAnchorLeft.x, slingAnchorLeft.y);
    sCtx.stroke();

    // Right Prong
    sCtx.beginPath();
    sCtx.moveTo(125, 220);
    sCtx.quadraticCurveTo(155, 205, slingAnchorRight.x, slingAnchorRight.y);
    sCtx.stroke();

    // Rubber Bands & Pocket
    if (!seedProjectile) {
      sCtx.lineWidth = 4;
      sCtx.strokeStyle = '#e11d48';

      // Left Band
      sCtx.beginPath();
      sCtx.moveTo(slingAnchorLeft.x, slingAnchorLeft.y);
      sCtx.lineTo(seedCurrent.x, seedCurrent.y);
      sCtx.stroke();

      // Right Band
      sCtx.beginPath();
      sCtx.moveTo(slingAnchorRight.x, slingAnchorRight.y);
      sCtx.lineTo(seedCurrent.x, seedCurrent.y);
      sCtx.stroke();

      // Heart Seed in Pocket
      drawHeartSeed(seedCurrent.x, seedCurrent.y, 16);

      // Trajectory Prediction Line if dragging
      if (isDraggingSeed) {
        const dx = seedOrigin.x - seedCurrent.x;
        const dy = seedOrigin.y - seedCurrent.y;
        sCtx.setLineDash([5, 5]);
        sCtx.lineWidth = 2;
        sCtx.strokeStyle = 'rgba(244, 63, 94, 0.6)';
        sCtx.beginPath();
        sCtx.moveTo(seedCurrent.x, seedCurrent.y);
        sCtx.quadraticCurveTo(
          seedOrigin.x + dx * 2,
          seedOrigin.y + dy * 2 - 60,
          seedOrigin.x + dx * 3.5,
          seedOrigin.y + dy * 3.5 - 180
        );
        sCtx.stroke();
        sCtx.setLineDash([]);
      }
    }

    // Flying Projectile Animation
    if (seedProjectile) {
      seedProjectile.progress += 0.035;
      const t = seedProjectile.progress;
      const px = Math.pow(1 - t, 2) * seedProjectile.p0.x + 2 * (1 - t) * t * seedProjectile.p1.x + Math.pow(t, 2) * seedProjectile.p2.x;
      const py = Math.pow(1 - t, 2) * seedProjectile.p0.y + 2 * (1 - t) * t * seedProjectile.p1.y + Math.pow(t, 2) * seedProjectile.p2.y;

      drawHeartSeed(px, py, 14 * (1 - t * 0.4));

      if (t >= 1) {
        seedProjectile = null;
        triggerPlantAndBloom();
      }
    }
  }

  function drawHeartSeed(x, y, size) {
    sCtx.save();
    sCtx.translate(x, y);
    sCtx.fillStyle = '#ff2a6d';
    sCtx.shadowColor = 'rgba(255, 42, 109, 0.8)';
    sCtx.shadowBlur = 12;

    sCtx.beginPath();
    const d = size / 2;
    sCtx.moveTo(0, d * 0.4);
    sCtx.bezierCurveTo(0, d * 0.9, -d, d * 1.2, -d * 1.2, d * 0.6);
    sCtx.bezierCurveTo(-d * 1.4, 0, -d * 0.6, -d * 0.5, 0, -d * 1.2);
    sCtx.bezierCurveTo(d * 0.6, -d * 0.5, d * 1.4, 0, d * 1.2, d * 0.6);
    sCtx.bezierCurveTo(d, d * 1.2, 0, d * 0.9, 0, d * 0.4);
    sCtx.fill();
    sCtx.restore();
  }

  // Slingshot Events
  function getSlingshotPos(e) {
    const rect = slingshotCanvas.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
    return {
      x: (clientX - rect.left) * (sWidth / rect.width),
      y: (clientY - rect.top) * (sHeight / rect.height)
    };
  }

  slingshotCanvas.addEventListener('mousedown', (e) => {
    if (isPlanted) return;
    const pos = getSlingshotPos(e);
    const dist = Math.hypot(pos.x - seedCurrent.x, pos.y - seedCurrent.y);
    if (dist < 50) {
      isDraggingSeed = true;
      playTwangSound();
    }
  });

  window.addEventListener('mousemove', (e) => {
    if (!isDraggingSeed) return;
    const pos = getSlingshotPos(e);
    const dx = pos.x - seedOrigin.x;
    const dy = pos.y - seedOrigin.y;
    const dist = Math.hypot(dx, dy);
    const maxPull = 90;

    if (dist > maxPull) {
      seedCurrent.x = seedOrigin.x + (dx / dist) * maxPull;
      seedCurrent.y = seedOrigin.y + (dy / dist) * maxPull;
    } else {
      seedCurrent.x = pos.x;
      seedCurrent.y = pos.y;
    }
  });

  window.addEventListener('mouseup', () => {
    if (!isDraggingSeed) return;
    isDraggingSeed = false;

    const pullDist = Math.hypot(seedCurrent.x - seedOrigin.x, seedCurrent.y - seedOrigin.y);
    if (pullDist > 25) {
      launchSlingshot();
    } else {
      seedCurrent = { ...seedOrigin };
    }
  });

  slingshotCanvas.addEventListener('touchstart', (e) => {
    if (isPlanted) return;
    const pos = getSlingshotPos(e);
    const dist = Math.hypot(pos.x - seedCurrent.x, pos.y - seedCurrent.y);
    if (dist < 60) {
      isDraggingSeed = true;
      playTwangSound();
    }
  }, { passive: true });

  window.addEventListener('touchmove', (e) => {
    if (!isDraggingSeed) return;
    const pos = getSlingshotPos(e);
    const dx = pos.x - seedOrigin.x;
    const dy = pos.y - seedOrigin.y;
    const dist = Math.hypot(dx, dy);
    const maxPull = 90;

    if (dist > maxPull) {
      seedCurrent.x = seedOrigin.x + (dx / dist) * maxPull;
      seedCurrent.y = seedOrigin.y + (dy / dist) * maxPull;
    } else {
      seedCurrent.x = pos.x;
      seedCurrent.y = pos.y;
    }
  }, { passive: true });

  window.addEventListener('touchend', () => {
    if (!isDraggingSeed) return;
    isDraggingSeed = false;
    const pullDist = Math.hypot(seedCurrent.x - seedOrigin.x, seedCurrent.y - seedOrigin.y);
    if (pullDist > 25) {
      launchSlingshot();
    } else {
      seedCurrent = { ...seedOrigin };
    }
  });

  function launchSlingshot() {
    playWhooshSound();
    seedProjectile = {
      progress: 0,
      p0: { x: seedCurrent.x, y: seedCurrent.y },
      p1: { x: seedOrigin.x + (seedOrigin.x - seedCurrent.x) * 2.5, y: -60 },
      p2: { x: sWidth + 120, y: -100 }
    };
    seedCurrent = { ...seedOrigin };
  }

  btnAutoShoot.addEventListener('click', () => {
    launchSlingshot();
  });

  // --- 5. Tree Growth & Milestone Sequence ---
  function triggerPlantAndBloom() {
    isPlanted = true;
    targetProgress = 1.0;
    stageIntro.classList.add('fade-out');

    // Play chime sound
    playChimeSound();
    if (!audioEnabled) {
      toggleMusicBox(true);
    }

    // Show Cinematic Banner
    setTimeout(() => {
      cinematicBanner.classList.remove('hidden');
      cinematicBanner.classList.add('show');
    }, 1200);

    // Transition banner & make greeting card pill available (keeps 3D view clean)
    setTimeout(() => {
      cinematicBanner.classList.remove('show');
      setTimeout(() => cinematicBanner.classList.add('hidden'), 800);

      if (btnShowCardPill) {
        btnShowCardPill.classList.remove('hidden');
      }
    }, 3800);
  }

  // --- 6. Web Audio API Acoustic Music Box Synthesizer ---
  function initAudio() {
    if (!audioCtx) {
      const AudioContext = window.AudioContext || window.webkitAudioContext;
      audioCtx = new AudioContext();
    }
    if (audioCtx.state === 'suspended') {
      audioCtx.resume();
    }
  }

  const bdayNotes = [
    { note: 261.63, dur: 0.35, pause: 0.1 },  // C4
    { note: 261.63, dur: 0.35, pause: 0.1 },  // C4
    { note: 293.66, dur: 0.6,  pause: 0.1 },  // D4
    { note: 261.63, dur: 0.6,  pause: 0.1 },  // C4
    { note: 349.23, dur: 0.6,  pause: 0.1 },  // F4
    { note: 329.63, dur: 1.0,  pause: 0.3 },  // E4

    { note: 261.63, dur: 0.35, pause: 0.1 },  // C4
    { note: 261.63, dur: 0.35, pause: 0.1 },  // C4
    { note: 293.66, dur: 0.6,  pause: 0.1 },  // D4
    { note: 261.63, dur: 0.6,  pause: 0.1 },  // C4
    { note: 392.00, dur: 0.6,  pause: 0.1 },  // G4
    { note: 349.23, dur: 1.0,  pause: 0.3 },  // F4

    { note: 261.63, dur: 0.35, pause: 0.1 },  // C4
    { note: 261.63, dur: 0.35, pause: 0.1 },  // C4
    { note: 523.25, dur: 0.6,  pause: 0.1 },  // C5
    { note: 440.00, dur: 0.6,  pause: 0.1 },  // A4
    { note: 349.23, dur: 0.6,  pause: 0.1 },  // F4
    { note: 329.63, dur: 0.6,  pause: 0.1 },  // E4
    { note: 293.66, dur: 0.9,  pause: 0.3 },  // D4

    { note: 466.16, dur: 0.35, pause: 0.1 },  // Bb4
    { note: 466.16, dur: 0.35, pause: 0.1 },  // Bb4
    { note: 440.00, dur: 0.6,  pause: 0.1 },  // A4
    { note: 349.23, dur: 0.6,  pause: 0.1 },  // F4
    { note: 392.00, dur: 0.6,  pause: 0.1 },  // G4
    { note: 349.23, dur: 1.2,  pause: 0.8 }   // F4
  ];

  let musicInterval = null;
  let noteIndex = 0;

  function playTone(freq, duration) {
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();

    osc.type = 'triangle';
    osc.frequency.setValueAtTime(freq, audioCtx.currentTime);

    // Music Box Exponential Pluck
    gain.gain.setValueAtTime(0.001, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.3, audioCtx.currentTime + 0.02);
    gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + duration);

    osc.connect(gain);
    gain.connect(audioCtx.destination);

    osc.start();
    osc.stop(audioCtx.currentTime + duration);
  }

  function playTwangSound() {
    initAudio();
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(160, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(280, audioCtx.currentTime + 0.12);
    gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
    gain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + 0.12);
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start();
    osc.stop(audioCtx.currentTime + 0.12);
  }

  function playWhooshSound() {
    initAudio();
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(450, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(150, audioCtx.currentTime + 0.25);
    gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
    gain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + 0.25);
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start();
    osc.stop(audioCtx.currentTime + 0.25);
  }

  function playChimeSound() {
    initAudio();
    if (!audioCtx) return;
    [523.25, 659.25, 783.99, 1046.50].forEach((f, i) => {
      setTimeout(() => playTone(f, 1.2), i * 140);
    });
  }

  function startMusicLoop() {
    if (musicInterval) clearInterval(musicInterval);
    noteIndex = 0;

    function step() {
      if (!audioEnabled) return;
      const n = bdayNotes[noteIndex];
      playTone(n.note, n.dur);
      noteIndex = (noteIndex + 1) % bdayNotes.length;
      musicInterval = setTimeout(step, (n.dur + n.pause) * 1000);
    }
    step();
  }

  function toggleMusicBox(forceState) {
    initAudio();
    audioEnabled = forceState !== undefined ? forceState : !audioEnabled;
    if (audioEnabled) {
      btnAudioToggle.innerHTML = '<i class="fas fa-volume-up"></i>';
      btnAudioToggle.classList.add('active');
      startMusicLoop();
    } else {
      btnAudioToggle.innerHTML = '<i class="fas fa-volume-mute"></i>';
      btnAudioToggle.classList.remove('active');
      if (musicInterval) clearTimeout(musicInterval);
    }
  }

  btnAudioToggle.addEventListener('click', () => toggleMusicBox());

  // --- 7. Milestone Clock Calculation ---
  function updateMilestoneClock() {
    const now = new Date();
    const diffMs = Math.max(0, now - birthDate);
    const totalSecs = Math.floor(diffMs / 1000);

    const days = Math.floor(totalSecs / 86400);
    const hours = Math.floor((totalSecs % 86400) / 3600);
    const mins = Math.floor((totalSecs % 3600) / 60);
    const secs = totalSecs % 60;

    cDays.textContent = days.toLocaleString();
    cHours.textContent = String(hours).padStart(2, '0');
    cMins.textContent = String(mins).padStart(2, '0');
    cSecs.textContent = String(secs).padStart(2, '0');
  }
  setInterval(updateMilestoneClock, 1000);
  updateMilestoneClock();

  // --- 8. Mouse / Touch Parallax & Orbit ---
  function onPointerDown(e) {
    isDraggingOrbit = true;
    prevMouseX = e.clientX;
    prevMouseY = e.clientY;
  }
  function onPointerMove(e) {
    const nx = (e.clientX / window.innerWidth) * 2 - 1;
    const ny = -(e.clientY / window.innerHeight) * 2 + 1;

    if (isDraggingOrbit) {
      const dx = e.clientX - prevMouseX;
      const dy = e.clientY - prevMouseY;
      orbitAngleX += dx * 0.005;
      orbitAngleY = Math.max(-0.6, Math.min(0.6, orbitAngleY + dy * 0.005));
      prevMouseX = e.clientX;
      prevMouseY = e.clientY;
    } else {
      targetCamX = nx * 3.5;
      targetCamY = 3.5 + ny * 2.0;
    }
  }
  function onPointerUp() {
    isDraggingOrbit = false;
  }

  function onTouchStart(e) {
    if (e.touches.length === 1) {
      isDraggingOrbit = true;
      prevMouseX = e.touches[0].clientX;
      prevMouseY = e.touches[0].clientY;
    }
  }
  function onTouchMove(e) {
    if (isDraggingOrbit && e.touches.length === 1) {
      const dx = e.touches[0].clientX - prevMouseX;
      const dy = e.touches[0].clientY - prevMouseY;
      orbitAngleX += dx * 0.006;
      orbitAngleY = Math.max(-0.6, Math.min(0.6, orbitAngleY + dy * 0.006));
      prevMouseX = e.touches[0].clientX;
      prevMouseY = e.touches[0].clientY;
    }
  }
  function onTouchEnd() {
    isDraggingOrbit = false;
  }

  // --- 9. Card Visibility & Replay Handlers ---
  function hideCard() {
    if (!bdayCard) return;
    bdayCard.classList.remove('show');
    setTimeout(() => {
      bdayCard.classList.add('hidden');
      if (isPlanted && btnShowCardPill) {
        btnShowCardPill.classList.remove('hidden');
      }
    }, 350);
    if (btnToggleCard) btnToggleCard.classList.remove('active');
  }

  function showCard() {
    if (!bdayCard) return;
    if (btnShowCardPill) btnShowCardPill.classList.add('hidden');
    bdayCard.classList.remove('hidden');
    setTimeout(() => bdayCard.classList.add('show'), 50);
    if (btnToggleCard) btnToggleCard.classList.add('active');
  }

  function toggleCard() {
    if (!bdayCard) return;
    if (bdayCard.classList.contains('hidden') || !bdayCard.classList.contains('show')) {
      showCard();
    } else {
      hideCard();
    }
  }

  if (btnCloseCard) btnCloseCard.addEventListener('click', hideCard);
  if (btnToggleCard) btnToggleCard.addEventListener('click', toggleCard);
  if (btnShowCardPill) btnShowCardPill.addEventListener('click', showCard);

  btnReplay.addEventListener('click', () => {
    isPlanted = false;
    treeProgress = 0.0;
    targetProgress = 0.0;
    stageIntro.classList.remove('fade-out');
    cinematicBanner.classList.remove('show');
    cinematicBanner.classList.add('hidden');
    bdayCard.classList.remove('show');
    bdayCard.classList.add('hidden');
    if (btnShowCardPill) btnShowCardPill.classList.add('hidden');
    if (btnToggleCard) btnToggleCard.classList.remove('active');
    seedCurrent = { ...seedOrigin };
    seedProjectile = null;
    buildProceduralTree();
    initFallingPetals();
  });

  btnPalette.addEventListener('click', () => {
    themeIndex = (themeIndex + 1) % themes.length;
    buildProceduralTree();
    initFallingPetals();
  });

  // Make a Wish Modal
  btnOpenWish.addEventListener('click', () => {
    wishModal.classList.remove('hidden');
    candleFlame.classList.remove('blown-out');
    tapFlameHint.classList.remove('hidden');
    wishCongrats.classList.add('hidden');
  });
  btnCloseWish.addEventListener('click', () => wishModal.classList.add('hidden'));

  candleFlame.addEventListener('click', () => {
    candleFlame.classList.add('blown-out');
    tapFlameHint.classList.add('hidden');
    wishCongrats.classList.remove('hidden');
    playChimeSound();
  });

  // Customize Modal
  btnOpenCustom.addEventListener('click', () => {
    document.getElementById('inpName').value = recipientName;
    document.getElementById('inpMsg').value = customMsg;
    customModal.classList.remove('hidden');
  });
  btnCloseCustom.addEventListener('click', () => customModal.classList.add('hidden'));

  btnSaveCustom.addEventListener('click', () => {
    const name = document.getElementById('inpName').value.trim();
    const dateVal = document.getElementById('inpDate').value;
    const themeVal = document.getElementById('inpTheme').value;
    const msgVal = document.getElementById('inpMsg').value.trim();

    if (name) {
      recipientName = name;
      document.getElementById('cardRecipient').textContent = recipientName;
    }
    if (dateVal) {
      birthDate = new Date(dateVal + 'T00:00:00');
      updateMilestoneClock();
    }
    if (msgVal) {
      customMsg = msgVal;
      document.getElementById('cardCustomMsg').textContent = customMsg;
    }
    if (themeVal) {
      const idx = themes.findIndex(t => t.name === themeVal);
      if (idx !== -1) themeIndex = idx;
    }

    customModal.classList.add('hidden');
    buildProceduralTree();
    initFallingPetals();
  });

  // Record GIF
  if (btnRecordGif) {
    btnRecordGif.addEventListener('click', () => {
      const origHtml = btnRecordGif.innerHTML;
      btnRecordGif.disabled = true;
      btnRecordGif.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 0/24';

      const frames = [];
      const totalFrames = 24;
      let count = 0;

      const recordTimer = setInterval(() => {
        count++;
        btnRecordGif.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${count}/${totalFrames}`;
        try {
          frames.push(threeCanvas.toDataURL('image/jpeg', 0.8));
        } catch(e) {}

        if (count >= totalFrames) {
          clearInterval(recordTimer);
          btnRecordGif.innerHTML = '<i class="fas fa-cog fa-spin"></i> GIF...';

          if (typeof gifshot !== 'undefined' && frames.length > 0) {
            gifshot.createGIF({
              images: frames,
              gifWidth: Math.min(window.innerWidth, 500),
              gifHeight: Math.min(window.innerHeight, 420),
              interval: 0.1,
              numFrames: totalFrames,
              sampleInterval: 10
            }, function(obj) {
              btnRecordGif.disabled = false;
              if (!obj.error) {
                const link = document.createElement('a');
                link.href = obj.image;
                link.download = `${recipientName.toLowerCase()}-3d-birthday-surprise.gif`;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                btnRecordGif.innerHTML = '<i class="fas fa-check text-success"></i> Saved!';
                setTimeout(() => { btnRecordGif.innerHTML = origHtml; }, 2500);
              } else {
                btnRecordGif.innerHTML = origHtml;
              }
            });
          } else {
            btnRecordGif.disabled = false;
            btnRecordGif.innerHTML = origHtml;
          }
        }
      }, 100);
    });
  }

  // --- 10. Main Animation Loop (60 FPS) ---
  function animate() {
    requestAnimationFrame(animate);

    // Tree Growth Interpolation
    if (treeProgress < targetProgress) {
      treeProgress += 0.008;
      if (treeProgress > targetProgress) treeProgress = targetProgress;
    }

    if (treeGroup) {
      // Scale tree with smooth spring easing
      const currentScale = Math.min(1.0, treeProgress * 1.15);
      treeGroup.scale.set(currentScale, currentScale, currentScale);

      // Petals Blossom
      petalMeshes.forEach(p => {
        if (treeProgress > p.bloomThreshold) {
          const petalGrowth = Math.min(1.0, (treeProgress - p.bloomThreshold) * 3.5);
          const s = p.targetScale * petalGrowth;
          p.mesh.scale.set(s, s, s);
          p.mesh.rotation.z += p.rotSpeed;
        } else {
          p.mesh.scale.set(0.001, 0.001, 0.001);
        }
      });
    }

    // Drifting Falling Petals
    if (isPlanted && treeProgress > 0.4) {
      const time = Date.now() * 0.002;
      fallingPetals.forEach(p => {
        p.mesh.position.y -= p.vy;
        p.mesh.position.x += Math.sin(time + p.swayOffset) * 0.02 + p.vx;
        p.mesh.position.z += Math.cos(time + p.swayOffset) * 0.015 + p.vz;
        p.mesh.rotation.x += p.rotX;
        p.mesh.rotation.y += p.rotY;
        p.mesh.rotation.z += p.rotZ;

        // Reset when falling below floor
        if (p.mesh.position.y < -7) {
          p.mesh.position.y = 15 + Math.random() * 5;
          p.mesh.position.x = (Math.random() - 0.5) * 24;
          p.mesh.position.z = (Math.random() - 0.5) * 14;
        }
      });
    }

    // Camera Lerping
    const radius = 26;
    const targetX = radius * Math.sin(orbitAngleX) + targetCamX;
    const targetZ = radius * Math.cos(orbitAngleX);
    const targetY = targetCamY + orbitAngleY * 10;

    camera.position.x += (targetX - camera.position.x) * 0.05;
    camera.position.y += (targetY - camera.position.y) * 0.05;
    camera.position.z += (targetZ - camera.position.z) * 0.05;
    camera.lookAt(0, 4.5, 0);

    // Render WebGL
    renderer.render(scene, camera);

    // Render 2D Slingshot
    renderSlingshot();
  }

  // --- Boot Experience ---
  initThree();
  initSlingshotCanvas();
  animate();
})();

3. Core JavaScript Concepts You Learned

Three.js WebGL

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Procedural 3D Trees

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Physics Slingshot

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Heart Shape Meshes

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Web Audio API

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Particle Wind Simulation

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

4. How to Run Locally on Your Computer

  1. Download the project ZIP: Click the Download Project (.ZIP) button at the top or in the live runner to save 3d-birthday-surprise-project.zip.
  2. Extract the archive: Unzip the downloaded file. Inside you will find separate index.html, style.css, script.js, index-standalone.html, and a comprehensive README.md.
  3. Open in any browser: Simply double-click index.html (or index-standalone.html) to open and test immediately in Chrome, Firefox, Safari, or Edge.
  4. Edit in Visual Studio Code: Open the extracted folder in VS Code. Right-click index.html and select "Open with Live Server" to enjoy instant live reloading as you modify HTML, CSS, or JS!

Bonus Challenges for Learners

Ready to level up? Try adding these extra features on your own:

  • Add sound effects or audio feedback for user button clicks.
  • Add custom theme color customizer with CSS variables.
  • Export the results to PDF or copy as a formatted text summary.

Other Beginner Projects You Might Like

Advanced45 mins
Creative & 3D WebGL

3D 'Add to Cart' Product Delivery Truck & Box Animation

Viral 3D WebGL e-commerce interaction: starts with a floating 3D 'Add to Cart πŸ›’' button in center stage. When clicked, it morphs into an unfolding cardboard box, glides to the side, and launches the parcel along a 3D parabolic trajectory curve into the open cargo bay of a yellow delivery truck, which seals its doors and drives off for express 24h delivery.

Three.js 3D WebGLInteractive 3D Button MorphingFlap Physics & Box Unfolding+4
Advanced35 mins
Creative & 3D WebGL

3D Radial Share Arc Button Animation

Viral UI component inspired by @_code_and_chill_: clicking the 'SHARE' capsule button triggers a smooth spring-physics radial arc splay of 5 social channel discs (𝕏, LinkedIn, WhatsApp, Email, Copy Link) using trigonometric CSS placement formulas.

Trigonometric Radial Arc Splay3D Interactive Perspective TiltSpring Physics & Harmonic Damping+3
Intermediate40 mins
Creative & 3D WebGL

3D Cosmic Memory Carousel & Flip Card Animation

A futuristic 3D cylindrical ring carousel in deep cosmic space: features 6 interactive holographic memory cards revolving in 3D perspective with touch/mouse drag, 3D card flip mechanics, neon glow shaders, and celebration effects β€οΈπŸŽ‚βœ¨.

CSS 3D Transform MatrixCylindrical Radial Coordinate Math3D Card Flip Mechanics+3