Birthday Heart Tree Animation

A magical animated birthday surprise with an organic tree blossoming into a heart-shaped canopy of glowing petals, live typewriter greeting, birthday melody music, and interactive heart fireworks.

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

Birthday Heart Tree Animation

live-sandbox://birthday-heart-animation

1. Project Overview & Features

The Birthday Heart Tree Animation 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:

  • Procedural fractal tree growth with natural branching physics
  • Heart-shaped blossom canopy blooming with 450+ multi-colored petals
  • Smooth typewriter greeting effect with customizable name and message
  • Built-in Web Audio API Happy Birthday song synthesizer & sound effects
  • Interactive canvas click/tap particle emitter for floating hearts
  • Direct Animated GIF recorder & exporter to save & share the animation
  • Celebration action dock: Download GIF, Replay, Confetti cannon, Music toggle, Edit greeting

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="birthday-surprise-wrap" id="birthdayWrap">
  <!-- Top Floating Action Toolbar -->
  <div class="surprise-toolbar">
    <div class="toolbar-brand">
      <span class="badge-hbd"><i class="fas fa-birthday-cake me-1"></i> Birthday Surprise</span>
      <span class="recipient-badge" id="badgeRecipientName">For: My Favorite Person ❀️</span>
    </div>
    <div class="toolbar-actions">
      <button type="button" class="btn-surprise btn-gif" id="btnDownloadGif" title="Record and download as animated GIF">
        <i class="fas fa-file-video me-1"></i> <span id="gifBtnText">Download GIF</span>
      </button>
      <button type="button" class="btn-surprise btn-music" id="btnToggleMusic" title="Play / Pause Birthday Song">
        <i class="fas fa-music"></i> <span id="musicBtnText">Play Music</span>
      </button>
      <button type="button" class="btn-surprise btn-confetti" id="btnConfettiBurst" title="Launch Confetti Blast">
        <i class="fas fa-sparkles"></i> Confetti
      </button>
      <button type="button" class="btn-surprise btn-edit" id="btnOpenCustomize" title="Personalize Greeting & Name">
        <i class="fas fa-edit"></i> Edit Wish
      </button>
      <button type="button" class="btn-surprise btn-replay" id="btnReplayAll" title="Replay Tree Growth Animation">
        <i class="fas fa-redo"></i> Replay
      </button>
    </div>
  </div>

  <!-- Main Animation Canvas & Greeting Container -->
  <div class="birthday-stage-container" id="stageContainer">
    
    <!-- Ambient Floating Bokeh / Sparkles -->
    <div class="ambient-particles" id="ambientParticles"></div>

    <!-- Left Column: Typewriter Greeting Card & Milestone Clock -->
    <div class="greeting-text-column" id="greetingColumn">
      <div class="greeting-card-box">
        <div class="greeting-header-bar">
          <div class="window-dots">
            <span class="dot-red"></span>
            <span class="dot-yellow"></span>
            <span class="dot-green"></span>
          </div>
          <span class="greeting-window-title"><i class="fas fa-heart text-danger me-1"></i> Birthday Wishes</span>
        </div>

        <div class="typewriter-content" id="typewriterBox">
          <div class="type-lines-container" id="typewriterText"></div>
          <span class="type-cursor" id="typeCursor">|</span>
        </div>

        <!-- Clock / Milestone Box (As in original) -->
        <div class="clock-milestone-box" id="clockBox">
          <div class="clock-icon"><i class="fas fa-gift"></i></div>
          <div class="clock-details">
            <span class="clock-label">Celebrating This Special Day:</span>
            <span class="clock-timer" id="clockTimer">00 days 00 hours 00 mins 00 secs</span>
          </div>
        </div>
      </div>
    </div>

    <!-- Right Column: HTML5 Canvas for Organic Tree Growth & Heart Blossoms -->
    <div class="canvas-tree-column">
      <canvas id="treeCanvas" width="680" height="580"></canvas>
      <div class="canvas-hint-pill" id="canvasHint">
        <i class="fas fa-hand-pointer me-1"></i> Click anywhere to spawn floating hearts πŸ’•
      </div>
    </div>
  </div>

  <!-- Customization Modal Overlay -->
  <div class="custom-modal-backdrop hidden" id="customModal">
    <div class="custom-modal-card">
      <div class="modal-header">
        <h3><i class="fas fa-magic text-warning me-2"></i> Personalize Birthday Surprise</h3>
        <button type="button" class="btn-close-modal" id="btnCloseModal"><i class="fas fa-times"></i></button>
      </div>
      <div class="modal-body">
        <div class="input-group-custom">
          <label for="inputRecipient">Recipient Name:</label>
          <input type="text" id="inputRecipient" value="My Favorite Person" placeholder="e.g. Sarah, Mom, Bestie, Alex">
        </div>
        <div class="input-group-custom">
          <label for="inputMessage">Birthday Message (Line by Line):</label>
          <textarea id="inputMessage" rows="6" placeholder="Enter greeting lines...">Hey you πŸ’•
Happy Birthday! πŸŽ‚βœ¨
May God bless you with endless joy & peace 🌸
And give you unlimited happiness & smiles πŸ’–
Just saying... you are truly amazing! 🌟
Sending good vibes, warm hugs & a big wink πŸ˜‰
Hope you have the most wonderful day today! πŸŽ‰πŸŽˆ
β€” Forever & Always ❀️</textarea>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn-modal-cancel" id="btnCancelModal">Cancel</button>
        <button type="button" class="btn-modal-save" id="btnSaveModal"><i class="fas fa-heart me-1"></i> Save & Replay</button>
      </div>
    </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;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

body {
  background: #fdf2f8;
  background-image: 
    radial-gradient(#fbcfe8 1px, transparent 1px),
    radial-gradient(#f472b6 1px, #fdf2f8 1px);
  background-size: 40px 40px;
  background-position: 0 0, 20px 20px;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  color: #1e293b;
  overflow-x: hidden;
}

.birthday-surprise-wrap {
  width: 100%;
  max-width: 1100px;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(244, 114, 182, 0.35);
  border-radius: 28px;
  padding: 24px;
  box-shadow: 0 20px 50px -10px rgba(236, 72, 153, 0.25), 0 0 30px rgba(244, 114, 182, 0.15);
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* Ambient Floating Particles Layer */
.ambient-particles {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
  z-index: 1;
  overflow: hidden;
}
.ambient-dot {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(244, 114, 182, 0.6) 0%, rgba(244, 114, 182, 0) 70%);
  animation: floatAmbient 6s infinite ease-in-out alternate;
}
@keyframes floatAmbient {
  0% { transform: translateY(0px) scale(0.8); opacity: 0.3; }
  100% { transform: translateY(-30px) scale(1.2); opacity: 0.8; }
}

/* Top Toolbar */
.surprise-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  background: #ffffff;
  padding: 10px 16px;
  border-radius: 18px;
  border: 1px solid #fbcfe8;
  box-shadow: 0 4px 12px rgba(244, 114, 182, 0.1);
  z-index: 10;
}
.toolbar-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.badge-hbd {
  background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%);
  color: #ffffff;
  padding: 5px 12px;
  border-radius: 20px;
  font-size: 0.82rem;
  font-weight: 700;
  box-shadow: 0 4px 10px rgba(236, 72, 153, 0.3);
}
.recipient-badge {
  font-size: 0.9rem;
  font-weight: 700;
  color: #be185d;
}
.toolbar-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.btn-surprise {
  background: #fff0f5;
  border: 1px solid #fbcfe8;
  color: #be185d;
  padding: 7px 14px;
  border-radius: 12px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-surprise:hover {
  background: #fce7f3;
  transform: translateY(-2px);
  box-shadow: 0 4px 10px rgba(236, 72, 153, 0.2);
}
.btn-gif {
  background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
  color: #ffffff;
  border-color: #7c3aed;
  box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3);
}
.btn-gif:hover {
  background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);
  box-shadow: 0 6px 15px rgba(99, 102, 241, 0.45);
}
.btn-gif.recording {
  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  border-color: #b91c1c;
  animation: pulseRecording 1s infinite alternate;
}
@keyframes pulseRecording {
  0% { transform: scale(1); box-shadow: 0 0 5px rgba(239, 68, 68, 0.5); }
  100% { transform: scale(1.05); box-shadow: 0 0 15px rgba(239, 68, 68, 0.8); }
}
.btn-music.playing {
  background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
  color: #ffffff;
  border-color: #db2777;
  box-shadow: 0 4px 12px rgba(236, 72, 153, 0.4);
  animation: pulseMusic 1.5s infinite alternate;
}
@keyframes pulseMusic {
  0% { transform: scale(1); }
  100% { transform: scale(1.04); }
}
.btn-confetti {
  background: #fdf4ff;
  border-color: #f0abfc;
  color: #a21caf;
}
.btn-confetti:hover {
  background: #fae8ff;
}

/* Stage Layout (Grid 2 Columns: Text Card + Canvas) */
.birthday-stage-container {
  display: grid;
  grid-template-columns: 1fr 1.25fr;
  gap: 20px;
  align-items: center;
  position: relative;
  z-index: 5;
}

@media (max-width: 900px) {
  .birthday-stage-container {
    grid-template-columns: 1fr;
  }
}

/* Greeting Card Box (Left Column) */
.greeting-text-column {
  width: 100%;
}
.greeting-card-box {
  background: #ffffff;
  border: 1px solid #fbcfe8;
  border-radius: 20px;
  padding: 24px;
  box-shadow: 0 10px 25px rgba(236, 72, 153, 0.12);
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 380px;
}
.greeting-header-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px dashed #fbcfe8;
  padding-bottom: 10px;
}
.window-dots {
  display: flex;
  gap: 6px;
}
.window-dots span {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
}
.dot-red { background: #f87171; }
.dot-yellow { background: #fbbf24; }
.dot-green { background: #4ade80; }
.greeting-window-title {
  font-size: 0.82rem;
  font-weight: 700;
  color: #9d174d;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* Typewriter Area */
.typewriter-content {
  min-height: 220px;
  font-size: 1.05rem;
  line-height: 1.8;
  color: #475569;
  font-family: 'Segoe UI', -apple-system, Roboto, sans-serif;
  position: relative;
}
.type-line-item {
  margin-bottom: 6px;
  font-weight: 600;
  color: #334155;
  opacity: 0;
  transform: translateY(4px);
  animation: lineFadeIn 0.3s forwards ease-out;
}
@keyframes lineFadeIn {
  to { opacity: 1; transform: translateY(0); }
}
.type-line-item:first-child {
  font-size: 1.25rem;
  font-weight: 800;
  color: #db2777;
}
.type-line-item:nth-child(2) {
  font-size: 1.35rem;
  font-weight: 800;
  color: #be185d;
  margin-bottom: 12px;
}
.type-line-item:last-child {
  font-weight: 800;
  color: #e11d48;
  margin-top: 10px;
}
.type-cursor {
  display: inline-block;
  color: #ec4899;
  font-weight: 800;
  animation: blinkCursor 0.8s infinite;
}
@keyframes blinkCursor {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

/* Milestone Clock Box */
.clock-milestone-box {
  background: linear-gradient(135deg, #fdf2f8 0%, #fce7f3 100%);
  border: 1px solid #fbcfe8;
  border-radius: 14px;
  padding: 12px 16px;
  display: flex;
  align-items: center;
  gap: 14px;
}
.clock-icon {
  width: 42px;
  height: 42px;
  background: #ffffff;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #ec4899;
  font-size: 1.2rem;
  box-shadow: 0 4px 8px rgba(236, 72, 153, 0.15);
}
.clock-details {
  display: flex;
  flex-direction: column;
}
.clock-label {
  font-size: 0.72rem;
  font-weight: 700;
  color: #9d174d;
  text-transform: uppercase;
}
.clock-timer {
  font-family: 'Courier New', Courier, monospace;
  font-size: 0.95rem;
  font-weight: 800;
  color: #be185d;
}

/* Canvas Tree Column (Right Column) */
.canvas-tree-column {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #ffffff;
  border: 1px solid #fbcfe8;
  border-radius: 20px;
  padding: 10px;
  box-shadow: 0 10px 25px rgba(236, 72, 153, 0.12);
  overflow: hidden;
}
#treeCanvas {
  width: 100%;
  height: auto;
  max-width: 680px;
  border-radius: 14px;
  background: radial-gradient(circle at 50% 30%, #ffffff 0%, #fff5f8 100%);
  cursor: pointer;
}
.canvas-hint-pill {
  position: absolute;
  bottom: 16px;
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(8px);
  border: 1px solid #fbcfe8;
  color: #db2777;
  font-size: 0.75rem;
  font-weight: 700;
  padding: 4px 12px;
  border-radius: 20px;
  pointer-events: none;
  box-shadow: 0 2px 8px rgba(236, 72, 153, 0.15);
  animation: pulseHint 2s infinite alternate;
}
@keyframes pulseHint {
  0% { transform: translateY(0); }
  100% { transform: translateY(-4px); }
}

/* Customization Modal */
.custom-modal-backdrop {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(15, 23, 42, 0.6);
  backdrop-filter: blur(6px);
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  transition: opacity 0.3s;
}
.custom-modal-backdrop.hidden {
  display: none;
}
.custom-modal-card {
  background: #ffffff;
  border-radius: 24px;
  width: 100%;
  max-width: 520px;
  padding: 24px;
  box-shadow: 0 20px 50px rgba(0,0,0,0.3);
  animation: modalScaleUp 0.25s ease-out;
}
@keyframes modalScaleUp {
  from { transform: scale(0.9); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.modal-header h3 {
  font-size: 1.2rem;
  font-weight: 800;
  color: #0f172a;
  margin: 0;
}
.btn-close-modal {
  background: transparent;
  border: none;
  font-size: 1.2rem;
  color: #94a3b8;
  cursor: pointer;
}
.modal-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 20px;
}
.input-group-custom label {
  display: block;
  font-size: 0.85rem;
  font-weight: 700;
  color: #334155;
  margin-bottom: 6px;
}
.input-group-custom input,
.input-group-custom textarea {
  width: 100%;
  padding: 10px 14px;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  font-size: 0.95rem;
  color: #1e293b;
  outline: none;
  transition: border-color 0.2s;
}
.input-group-custom input:focus,
.input-group-custom textarea:focus {
  border-color: #ec4899;
  box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.15);
}
.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.btn-modal-cancel {
  background: #f1f5f9;
  border: none;
  padding: 10px 18px;
  border-radius: 10px;
  font-weight: 700;
  color: #64748b;
  cursor: pointer;
}
.btn-modal-save {
  background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
  color: #ffffff;
  border: none;
  padding: 10px 20px;
  border-radius: 10px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(236, 72, 153, 0.3);
}

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)
/**
 * Birthday Heart Tree Animation Engine
 * Procedural Fractal Tree Growth, Blooming Heart Canopy, Typewriter & Audio Synthesizer
 */

const canvas = document.getElementById('treeCanvas');
const ctx = canvas.getContext('2d');

// State Variables
let treeBranches = [];
let treeBlossoms = [];
let fallingPetals = [];
let interactiveHearts = [];
let confettiParticles = [];
let treeGrowthProgress = 0;
let isTreeGrowing = true;
let isBlossoming = false;
let animFrameId = null;
let milestoneSeconds = 0;
let isRecordingGif = false;

// Typewriter Configuration
let recipientName = "My Favorite Person";
let greetingLines = [
  "Hey you πŸ’•",
  "Happy Birthday! πŸŽ‚βœ¨",
  "May God bless you with endless joy & peace 🌸",
  "And give you unlimited happiness & smiles πŸ’–",
  "Just saying... you are truly amazing! 🌟",
  "Sending good vibes, warm hugs & a big wink πŸ˜‰",
  "Hope you have the most wonderful day today! πŸŽ‰πŸŽˆ",
  "β€” Forever & Always ❀️"
];

let typeLineIndex = 0;
let typeCharIndex = 0;
let typeTimeout = null;

// Audio Synthesizer State
let isPlayingMusic = false;
let audioCtx = null;
let musicInterval = null;

// Palette for Heart Canopy Blossoms
const HEART_COLORS = [
  '#ff4d6d', '#f72585', '#ff758f', '#ff85a1', 
  '#ff99c8', '#ffb703', '#fb8500', '#f15bb5', 
  '#fee440', '#e63946', '#ffb3c1', '#f43f5e'
];

/**
 * 1. Parametric Heart Formula
 */
function getHeartPosition(t, scale) {
  const x = 16 * Math.pow(Math.sin(t), 3);
  const y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
  return { x: x * scale, y: y * scale };
}

/**
 * Draw a single 2D heart shape on canvas
 */
function drawHeart(c, x, y, size, color, angle = 0, alpha = 1) {
  c.save();
  c.translate(x, y);
  c.rotate(angle);
  c.scale(size / 15, size / 15);
  c.globalAlpha = Math.max(0, Math.min(1, alpha));
  c.fillStyle = color;

  c.beginPath();
  c.moveTo(0, 0);
  c.bezierCurveTo(-10, -10, -20, 5, 0, 18);
  c.bezierCurveTo(20, 5, 10, -10, 0, 0);
  c.closePath();
  c.fill();
  c.restore();
}

/**
 * 2. Generate Tree Structure & Heart Blossom Coordinates
 */
function initTreeData() {
  treeBranches = [];
  treeBlossoms = [];
  fallingPetals = [];
  interactiveHearts = [];

  const startX = canvas.width * 0.5;
  const startY = canvas.height - 25;
  const heartCenterY = canvas.height * 0.40;
  const heartCenterX = canvas.width * 0.5;

  // Build Procedural Branches
  buildBranch(startX, startY, -Math.PI / 2, 115, 13, 0);

  // Generate 420 Heart-shaped Blossoms inside heart envelope
  const totalBlossoms = 420;
  for (let i = 0; i < totalBlossoms; i++) {
    const t = Math.random() * Math.PI * 2;
    const r = Math.sqrt(Math.random()); // Even radial distribution
    const heartPoint = getHeartPosition(t, 12.5);

    const targetX = heartCenterX + heartPoint.x * r + (Math.random() * 8 - 4);
    const targetY = heartCenterY + heartPoint.y * r + (Math.random() * 8 - 4);

    treeBlossoms.push({
      x: targetX,
      y: targetY,
      size: Math.random() * 7 + 7,
      color: HEART_COLORS[Math.floor(Math.random() * HEART_COLORS.length)],
      angle: (Math.random() - 0.5) * 0.8,
      bloomDelay: Math.random() * 90 + 30, // frames delay
      bloomProgress: 0,
      scale: 0
    });
  }
}

function buildBranch(x, y, angle, length, width, depth) {
  const endX = x + Math.cos(angle) * length;
  const endY = y + Math.sin(angle) * length;

  treeBranches.push({
    startX: x,
    startY: y,
    endX: endX,
    endY: endY,
    width: width,
    depth: depth,
    growth: 0,
    maxGrowth: 1
  });

  if (depth >= 4 || length < 25) return;

  const numSubBranches = depth === 0 ? 3 : (Math.random() > 0.3 ? 2 : 3);
  for (let i = 0; i < numSubBranches; i++) {
    const spread = (i - (numSubBranches - 1) / 2) * 0.42 + (Math.random() * 0.2 - 0.1);
    const newLength = length * (0.65 + Math.random() * 0.15);
    const newWidth = Math.max(width * 0.65, 2.5);
    buildBranch(endX, endY, angle + spread, newLength, newWidth, depth + 1);
  }
}

/**
 * 3. Animation Loop (Tree Growth, Petal Flutter, Confetti)
 */
function animateScene() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Soft grass / trunk mound
  ctx.fillStyle = '#fce7f3';
  ctx.beginPath();
  ctx.ellipse(canvas.width * 0.5, canvas.height - 15, 110, 16, 0, 0, Math.PI * 2);
  ctx.fill();

  // Draw Branches
  let allBranchesDone = true;
  treeBranches.forEach(b => {
    if (b.growth < b.maxGrowth) {
      b.growth += 0.04;
      if (b.growth > b.maxGrowth) b.growth = b.maxGrowth;
      allBranchesDone = false;
    }

    const currX = b.startX + (b.endX - b.startX) * b.growth;
    const currY = b.startY + (b.endY - b.startY) * b.growth;

    ctx.save();
    ctx.strokeStyle = '#c08497';
    ctx.lineWidth = Math.max(b.width * (1 - b.depth * 0.15), 2);
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(b.startX, b.startY);
    ctx.lineTo(currX, currY);
    ctx.stroke();
    ctx.restore();
  });

  if (allBranchesDone) {
    isBlossoming = true;
  }

  // Draw Heart Canopy Blossoms
  if (isBlossoming) {
    treeBlossoms.forEach(blossom => {
      if (blossom.bloomDelay > 0) {
        blossom.bloomDelay--;
      } else if (blossom.bloomProgress < 1) {
        blossom.bloomProgress += 0.05;
        if (blossom.bloomProgress > 1) blossom.bloomProgress = 1;
        // Elastic bounce scale
        blossom.scale = (1 - Math.cos(blossom.bloomProgress * Math.PI)) * 0.5;
      }

      if (blossom.scale > 0) {
        drawHeart(ctx, blossom.x, blossom.y, blossom.size * blossom.scale, blossom.color, blossom.angle);
      }
    });

    // Spawn falling petals intermittently
    if (Math.random() < 0.25) {
      const randBlossom = treeBlossoms[Math.floor(Math.random() * treeBlossoms.length)];
      if (randBlossom && randBlossom.scale >= 0.8) {
        fallingPetals.push({
          x: randBlossom.x,
          y: randBlossom.y,
          size: Math.random() * 5 + 6,
          color: randBlossom.color,
          vx: (Math.random() - 0.5) * 1.5,
          vy: Math.random() * 1.2 + 0.8,
          angle: Math.random() * Math.PI * 2,
          vAngle: (Math.random() - 0.5) * 0.05,
          alpha: 1,
          sway: Math.random() * Math.PI * 2
        });
      }
    }
  }

  // Update & Draw Falling Petals
  for (let i = fallingPetals.length - 1; i >= 0; i--) {
    const p = fallingPetals[i];
    p.sway += 0.03;
    p.x += p.vx + Math.sin(p.sway) * 0.8;
    p.y += p.vy;
    p.angle += p.vAngle;

    if (p.y > canvas.height - 20) {
      p.alpha -= 0.02;
    }

    drawHeart(ctx, p.x, p.y, p.size, p.color, p.angle, p.alpha);

    if (p.alpha <= 0 || p.y > canvas.height + 20) {
      fallingPetals.splice(i, 1);
    }
  }

  // Interactive Clicked Hearts
  for (let i = interactiveHearts.length - 1; i >= 0; i--) {
    const h = interactiveHearts[i];
    h.x += h.vx;
    h.y += h.vy;
    h.vy -= 0.08; // float upwards
    h.alpha -= 0.015;
    h.size *= 0.99;

    drawHeart(ctx, h.x, h.y, h.size, h.color, h.angle, h.alpha);

    if (h.alpha <= 0) {
      interactiveHearts.splice(i, 1);
    }
  }

  // Confetti Blast Particles
  for (let i = confettiParticles.length - 1; i >= 0; i--) {
    const c = confettiParticles[i];
    c.x += c.vx;
    c.y += c.vy;
    c.vy += 0.2; // gravity
    c.vx *= 0.98;
    c.rotation += c.vRot;
    c.life--;

    ctx.save();
    ctx.translate(c.x, c.y);
    ctx.rotate(c.rotation);
    ctx.fillStyle = c.color;
    ctx.fillRect(-c.width / 2, -c.height / 2, c.width, c.height);
    ctx.restore();

    if (c.life <= 0 || c.y > canvas.height + 20) {
      confettiParticles.splice(i, 1);
    }
  }

  animFrameId = requestAnimationFrame(animateScene);
}

/**
 * 4. Typewriter Greeting Animation
 */
function startTypewriter() {
  clearTimeout(typeTimeout);
  const container = document.getElementById('typewriterText');
  container.innerHTML = '';
  typeLineIndex = 0;
  typeCharIndex = 0;

  typeNextChar();
}

function typeNextChar() {
  if (typeLineIndex >= greetingLines.length) return;

  const currentLineText = greetingLines[typeLineIndex];
  const container = document.getElementById('typewriterText');

  let currentLineElem = container.children[typeLineIndex];
  if (!currentLineElem) {
    currentLineElem = document.createElement('div');
    currentLineElem.className = 'type-line-item';
    container.appendChild(currentLineElem);
  }

  if (typeCharIndex < currentLineText.length) {
    currentLineElem.textContent += currentLineText.charAt(typeCharIndex);
    typeCharIndex++;
    typeTimeout = setTimeout(typeNextChar, Math.random() * 25 + 25);
  } else {
    typeLineIndex++;
    typeCharIndex = 0;
    typeTimeout = setTimeout(typeNextChar, 400);
  }
}

/**
 * 5. Milestone Clock Loop
 */
function updateMilestoneClock() {
  milestoneSeconds++;
  const days = Math.floor(milestoneSeconds / 86400);
  const hrs = Math.floor((milestoneSeconds % 86400) / 3600);
  const mins = Math.floor((milestoneSeconds % 3600) / 60);
  const secs = milestoneSeconds % 60;

  const clockTimer = document.getElementById('clockTimer');
  if (clockTimer) {
    clockTimer.textContent = `${days} days ${String(hrs).padStart(2,'0')} hours ${String(mins).padStart(2,'0')} mins ${String(secs).padStart(2,'0')} secs`;
  }
}
setInterval(updateMilestoneClock, 1000);

/**
 * 6. Web Audio API Happy Birthday Melodic Synthesizer
 */
function getAudioContext() {
  if (!audioCtx) {
    const AudioContextClass = window.AudioContext || window.webkitAudioContext;
    if (AudioContextClass) audioCtx = new AudioContextClass();
  }
  if (audioCtx && audioCtx.state === 'suspended') {
    audioCtx.resume();
  }
  return audioCtx;
}

// "Happy Birthday to You" Melody Notes & Durations (Note, Duration in ms)
const BIRTHDAY_SONG = [
  { f: 261.63, d: 350 }, // C4
  { f: 261.63, d: 250 }, // C4
  { f: 293.66, d: 500 }, // D4
  { f: 261.63, d: 500 }, // C4
  { f: 349.23, d: 500 }, // F4
  { f: 329.63, d: 900 }, // E4
  { f: 0,      d: 250 }, // pause
  { f: 261.63, d: 350 }, // C4
  { f: 261.63, d: 250 }, // C4
  { f: 293.66, d: 500 }, // D4
  { f: 261.63, d: 500 }, // C4
  { f: 392.00, d: 500 }, // G4
  { f: 349.23, d: 900 }, // F4
  { f: 0,      d: 250 }, // pause
  { f: 261.63, d: 350 }, // C4
  { f: 261.63, d: 250 }, // C4
  { f: 523.25, d: 500 }, // C5
  { f: 440.00, d: 500 }, // A4
  { f: 349.23, d: 500 }, // F4
  { f: 329.63, d: 500 }, // E4
  { f: 293.66, d: 700 }, // D4
  { f: 0,      d: 250 }, // pause
  { f: 466.16, d: 350 }, // Bb4
  { f: 466.16, d: 250 }, // Bb4
  { f: 440.00, d: 500 }, // A4
  { f: 349.23, d: 500 }, // F4
  { f: 392.00, d: 500 }, // G4
  { f: 349.23, d: 1200}  // F4
];

let songIndex = 0;
let songTimeout = null;

function playSongNote() {
  if (!isPlayingMusic) return;

  const ctx = getAudioContext();
  if (!ctx) return;

  const note = BIRTHDAY_SONG[songIndex];
  if (note.f > 0) {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(note.f, ctx.currentTime);

    gain.gain.setValueAtTime(0.18, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + (note.d / 1000));

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

    osc.start();
    osc.stop(ctx.currentTime + (note.d / 1000));
  }

  songIndex = (songIndex + 1) % BIRTHDAY_SONG.length;
  songTimeout = setTimeout(playSongNote, note.d + 50);
}

function toggleMusic() {
  const btn = document.getElementById('btnToggleMusic');
  const txt = document.getElementById('musicBtnText');

  if (!isPlayingMusic) {
    isPlayingMusic = true;
    btn.classList.add('playing');
    txt.textContent = 'Pause Music';
    songIndex = 0;
    playSongNote();
  } else {
    isPlayingMusic = false;
    clearTimeout(songTimeout);
    btn.classList.remove('playing');
    txt.textContent = 'Play Music';
  }
}

/**
 * 7. Confetti Cannon
 */
function launchConfetti() {
  const colors = ['#ec4899', '#f43f5e', '#facc15', '#38bdf8', '#a855f7', '#10b981'];
  for (let i = 0; i < 90; i++) {
    confettiParticles.push({
      x: canvas.width * 0.5 + (Math.random() * 80 - 40),
      y: canvas.height * 0.3,
      vx: (Math.random() - 0.5) * 16,
      vy: (Math.random() - 0.7) * 14,
      width: Math.random() * 8 + 6,
      height: Math.random() * 6 + 4,
      color: colors[Math.floor(Math.random() * colors.length)],
      rotation: Math.random() * Math.PI * 2,
      vRot: (Math.random() - 0.5) * 0.2,
      life: Math.random() * 80 + 90
    });
  }
}

/**
 * 8. Interactive Click Spawn Hearts
 */
canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  const x = (e.clientX - rect.left) * scaleX;
  const y = (e.clientY - rect.top) * scaleY;

  for (let i = 0; i < 12; i++) {
    interactiveHearts.push({
      x: x,
      y: y,
      size: Math.random() * 10 + 10,
      color: HEART_COLORS[Math.floor(Math.random() * HEART_COLORS.length)],
      vx: (Math.random() - 0.5) * 6,
      vy: (Math.random() - 0.5) * 6 - 2,
      angle: (Math.random() - 0.5) * 0.5,
      alpha: 1
    });
  }
});

/**
 * 9. Replay All Animations
 */
function replayAll() {
  isBlossoming = false;
  initTreeData();
  startTypewriter();
  launchConfetti();
}

/**
 * 10. Record & Download Animated GIF
 */
function recordAndDownloadGif() {
  if (isRecordingGif) return;
  const btn = document.getElementById('btnDownloadGif');
  const btnText = document.getElementById('gifBtnText');

  isRecordingGif = true;
  btn.classList.add('recording');
  btn.disabled = true;

  const capturedFrames = [];
  const totalFrames = 28;
  const frameDelay = 100; // 10 fps
  let capturedCount = 0;

  // Render on offscreen canvas with nice background and recipient watermark
  const offscreen = document.createElement('canvas');
  offscreen.width = 540;
  offscreen.height = 460;
  const octx = offscreen.getContext('2d');

  const recordInterval = setInterval(() => {
    capturedCount++;
    btnText.textContent = `Recording ${capturedCount}/${totalFrames}...`;

    // Composite background + tree canvas
    octx.fillStyle = '#fdf2f8';
    octx.fillRect(0, 0, offscreen.width, offscreen.height);
    octx.drawImage(canvas, 0, 0, offscreen.width, offscreen.height);

    // Add cute subtitle watermark at bottom
    octx.fillStyle = '#db2777';
    octx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
    octx.textAlign = 'center';
    octx.fillText(`Happy Birthday, ${recipientName}! ❀️✨`, offscreen.width / 2, offscreen.height - 14);

    capturedFrames.push(offscreen.toDataURL('image/jpeg', 0.85));

    if (capturedCount >= totalFrames) {
      clearInterval(recordInterval);
      btnText.textContent = 'Rendering GIF...';

      if (typeof gifshot !== 'undefined') {
        gifshot.createGIF({
          images: capturedFrames,
          gifWidth: 540,
          gifHeight: 460,
          interval: 0.1,
          numFrames: totalFrames,
          sampleInterval: 10
        }, function(obj) {
          isRecordingGif = false;
          btn.classList.remove('recording');
          btn.disabled = false;

          if (!obj.error) {
            const link = document.createElement('a');
            link.href = obj.image;
            const safeName = recipientName.toLowerCase().replace(/[^a-z0-9]/g, '-');
            link.download = `birthday-heart-surprise-${safeName}.gif`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);

            btnText.textContent = 'GIF Downloaded! πŸŽ‰';
            setTimeout(() => { btnText.textContent = 'Download GIF'; }, 3500);
          } else {
            downloadFallbackSnapshot();
          }
        });
      } else {
        downloadFallbackSnapshot();
      }
    }
  }, frameDelay);

  function downloadFallbackSnapshot() {
    isRecordingGif = false;
    btn.classList.remove('recording');
    btn.disabled = false;

    const link = document.createElement('a');
    link.href = canvas.toDataURL('image/png');
    const safeName = recipientName.toLowerCase().replace(/[^a-z0-9]/g, '-');
    link.download = `birthday-heart-${safeName}.png`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);

    btnText.textContent = 'Saved Card! 🎁';
    setTimeout(() => { btnText.textContent = 'Download GIF'; }, 3500);
  }
}

/**
 * 11. Event Listeners & Modal Handlers
 */
document.getElementById('btnDownloadGif').addEventListener('click', recordAndDownloadGif);
document.getElementById('btnToggleMusic').addEventListener('click', toggleMusic);
document.getElementById('btnConfettiBurst').addEventListener('click', launchConfetti);
document.getElementById('btnReplayAll').addEventListener('click', replayAll);

const modal = document.getElementById('customModal');
document.getElementById('btnOpenCustomize').addEventListener('click', () => {
  modal.classList.remove('hidden');
});
document.getElementById('btnCloseModal').addEventListener('click', () => {
  modal.classList.add('hidden');
});
document.getElementById('btnCancelModal').addEventListener('click', () => {
  modal.classList.add('hidden');
});

document.getElementById('btnSaveModal').addEventListener('click', () => {
  const nameInput = document.getElementById('inputRecipient').value.trim();
  const msgInput = document.getElementById('inputMessage').value.trim();

  if (nameInput) {
    recipientName = nameInput;
    document.getElementById('badgeRecipientName').textContent = `For: ${recipientName} ❀️`;
  }

  if (msgInput) {
    greetingLines = msgInput.split('\n').map(l => l.trim()).filter(l => l.length > 0);
  }

  modal.classList.add('hidden');
  replayAll();
});

// Ambient Bokeh Dots Generator
function createAmbientDots() {
  const container = document.getElementById('ambientParticles');
  if (!container) return;
  for (let i = 0; i < 15; i++) {
    const dot = document.createElement('div');
    dot.className = 'ambient-dot';
    const size = Math.random() * 70 + 40;
    dot.style.width = `${size}px`;
    dot.style.height = `${size}px`;
    dot.style.left = `${Math.random() * 100}%`;
    dot.style.top = `${Math.random() * 100}%`;
    dot.style.animationDelay = `${Math.random() * 4}s`;
    dot.style.animationDuration = `${Math.random() * 4 + 5}s`;
    container.appendChild(dot);
  }
}

// Initial Boot
createAmbientDots();
initTreeData();
startTypewriter();
animateScene();

3. Core JavaScript Concepts You Learned

HTML5 Canvas Tree Growth

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

Heart Equation Math & Bloom

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

Typewriter Text Animation

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

Web Audio API Birthday Chime

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

Floating Petals & Confetti

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 birthday-heart-animation-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

AnimatedBeginner20 mins
UI & Animations

Interactive Animated Login Form

A playful animated login form with an interactive mascot avatar that tracks your typing and covers its eyes on password input.

DOM Focus & Blur EventsCSS Transform & TransitionsPassword Peek Toggle+2
AnimatedBeginner25 mins
UI & Animations

Light Switch & Lamp Turn On Login Form

A dramatic interactive login page with a 3D hanging lamp and pull cord that turns on the spotlight to illuminate the dark room.

Dynamic CSS Conical Gradients & GlowWeb Audio Synthesis (Click SFX)DOM State & Class Transitions+2
AnimatedBeginner20 mins
UI & Animations

Folding Paper Plane Subscribe Animation

A creative subscribe button animation where clicking Send folds the button into an origami paper airplane that flies away.

CSS 3D Transforms & KeyframesSVG Origami 3D Facet FoldsMulti-Stage State Machine (Input βž” Folding βž” Sending βž” Subscribed)+2