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.
<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.
* {
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:
/**
* 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
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
4. How to Run Locally on Your Computer
- 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. - Extract the archive: Unzip the downloaded file. Inside you will find separate
index.html,style.css,script.js,index-standalone.html, and a comprehensiveREADME.md. - Open in any browser: Simply double-click
index.html(orindex-standalone.html) to open and test immediately in Chrome, Firefox, Safari, or Edge. - Edit in Visual Studio Code: Open the extracted folder in VS Code. Right-click
index.htmland 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.