1. Project Overview & Features
The Special Birthday Code 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:
- Procedural organic heart tree blooming with 500+ multi-shade fluttering petals
- macOS styled code terminal window with realistic line-by-line syntax-highlighted typing
- Live milestone clock tracking real-time celebration days, hours, minutes, seconds
- Web Audio API romantic & birthday music-box synthesizer (no external audio required)
- Interactive 3D gift box with unwrap explosion and floating heart fireworks
- One-click Animated GIF recorder to save and share the surprise on WhatsApp / Social Media
- Customization dock to personalize recipient name, poem, music, and background themes
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="special-gift-app" id="specialGiftApp">
<!-- Top Floating Celebration Bar -->
<div class="gift-top-bar">
<div class="brand-pill">
<span class="sparkle-icon">β¨</span>
<span class="gift-heading">A Special Birthday Gift</span>
<span class="dedication-tag" id="dedicationTag">To: Someone Very Special π</span>
</div>
<div class="bar-btn-group">
<button type="button" class="bar-btn btn-export-gif" id="btnRecordCardGif" title="Download as Animated GIF">
<i class="fas fa-file-video"></i> <span id="gifExportText">Download GIF</span>
</button>
<button type="button" class="bar-btn btn-music-play" id="btnAudioMelody" title="Toggle Musical Chimes">
<i class="fas fa-music"></i> <span id="audioStatusText">Play Music</span>
</button>
<button type="button" class="bar-btn btn-gift-unwrap" id="btnUnwrapGift" title="Unwrap Birthday Gift">
<i class="fas fa-gift"></i> Surprise
</button>
<button type="button" class="bar-btn btn-edit-wish" id="btnOpenWishModal" title="Edit Poem & Name">
<i class="fas fa-pen"></i> Edit Wish
</button>
<button type="button" class="bar-btn btn-replay-tree" id="btnReplaySurprise" title="Replay Animation">
<i class="fas fa-redo"></i> Replay
</button>
</div>
</div>
<!-- The Main Stage (#wrap and #main structure) -->
<div id="main" class="main-canvas-wrapper">
<div id="wrap" class="heart-stage-layout">
<!-- Left: macOS Styled Code / Poem Window -->
<div id="text" class="code-terminal-card">
<div class="terminal-top-bar">
<div class="terminal-dots">
<span class="t-dot dot-red"></span>
<span class="t-dot dot-yellow"></span>
<span class="t-dot dot-green"></span>
</div>
<span class="terminal-file-name"><i class="fab fa-html5 text-danger me-1"></i> index.html</span>
</div>
<div id="code" class="code-editor-lines">
<!-- Typewriter content typed out with syntax highlight spans -->
<div class="typed-code-content" id="typedCodeBlock"></div>
<span class="blinking-code-cursor" id="blinkingCursor">_</span>
</div>
<!-- Clock / Milestone Box (Exact match from screenshot) -->
<div id="clock-box" class="milestone-clock-card">
<div class="clock-badge-icon"><i class="fas fa-hourglass-half"></i></div>
<div class="clock-text-wrap">
<div class="clock-title">Days & Time Celebrated:</div>
<span id="clock" class="clock-display-live">365 days 0 hours 0 minutes 0 seconds</span>
</div>
</div>
</div>
<!-- Right: Pure HTML5 Tree & Blooming Heart Canopy Canvas -->
<div class="canvas-wrapper-box">
<canvas id="canvas" width="680" height="580"></canvas>
<div class="canvas-interactive-toast" id="interactiveToast">
<i class="fas fa-heart text-danger"></i> Move cursor for sparkling heart dust β’ Click to blast fireworks!
</div>
</div>
</div>
</div>
<!-- 3D Gift Box Modal (Unwrap Surprise) -->
<div class="gift-modal-overlay hidden" id="giftModal">
<div class="gift-surprise-card">
<div class="gift-box-3d" id="giftBoxAnimation">
<div class="box-lid">π</div>
</div>
<h2 class="surprise-title">Happy Birthday! πβ¨</h2>
<p class="surprise-quote">"I turned HTML, CSS, and JavaScript into a special birthday surprise. A special gift, made with time, effort, and a little bit of code."</p>
<button type="button" class="btn-close-gift" id="btnCloseGiftModal">Love It! β€οΈ</button>
</div>
</div>
<!-- Customization Modal -->
<div class="wish-modal-overlay hidden" id="wishModal">
<div class="wish-card-dialog">
<div class="wish-dialog-header">
<h3><i class="fas fa-heart text-danger me-2"></i> Customize Your Birthday Code Gift</h3>
<button type="button" class="btn-close-x" id="btnCloseWishModal"><i class="fas fa-times"></i></button>
</div>
<div class="wish-dialog-body">
<div class="wish-field">
<label for="recipientInput">Recipient's Name / Nickname:</label>
<input type="text" id="recipientInput" value="My Favorite Person" placeholder="e.g. Sarah, Alex, Mom, Bestie">
</div>
<div class="wish-field">
<label for="poemInput">Custom Code Lines (HTML/Poem):</label>
<textarea id="poemInput" rows="7" placeholder="Write line by line...">Hey you π
Happy Birthday πβ¨
May God bless you πΈ
And give u many happiness π
Just saying... you're pretty awesome π
Sending good vibes and maybe a wink π
Hope u have a great day today π
I turned HTML, CSS, and JavaScript into a special birthday surprise.
A special gift, made with time, effort, and a little bit of code. β¨
β Forever & Always β€οΈ</textarea>
</div>
</div>
<div class="wish-dialog-footer">
<button type="button" class="btn-wish-cancel" id="btnCancelWishModal">Cancel</button>
<button type="button" class="btn-wish-submit" id="btnSaveWishModal"><i class="fas fa-sparkles me-1"></i> Update & 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: #0f172a;
background-image:
radial-gradient(rgba(244, 114, 182, 0.15) 1px, transparent 1px),
radial-gradient(rgba(236, 72, 153, 0.1) 1px, #0f172a 1px);
background-size: 30px 30px;
background-position: 0 0, 15px 15px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
color: #f8fafc;
overflow-x: hidden;
}
.special-gift-app {
width: 100%;
max-width: 1160px;
background: #1e293b;
border: 1px solid #334155;
border-radius: 24px;
padding: 22px;
box-shadow: 0 25px 60px -10px rgba(0, 0, 0, 0.7), 0 0 40px rgba(244, 114, 182, 0.15);
display: flex;
flex-direction: column;
gap: 18px;
position: relative;
}
/* Top Toolbar */
.gift-top-bar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
background: #0f172a;
padding: 10px 16px;
border-radius: 16px;
border: 1px solid #334155;
}
.brand-pill {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.sparkle-icon {
font-size: 1.2rem;
animation: pulseSparkle 1.5s infinite alternate;
}
@keyframes pulseSparkle {
0% { transform: scale(1) rotate(0deg); }
100% { transform: scale(1.25) rotate(15deg); }
}
.gift-heading {
font-size: 1.05rem;
font-weight: 800;
background: linear-gradient(90deg, #f43f5e 0%, #ec4899 50%, #fb7185 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.dedication-tag {
background: rgba(244, 63, 94, 0.15);
border: 1px solid rgba(244, 63, 94, 0.3);
color: #fb7185;
font-size: 0.8rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 20px;
}
.bar-btn-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.bar-btn {
background: #1e293b;
border: 1px solid #334155;
color: #cbd5e1;
padding: 7px 14px;
border-radius: 10px;
font-size: 0.82rem;
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);
}
.bar-btn:hover {
background: #334155;
color: #ffffff;
transform: translateY(-2px);
}
.btn-export-gif {
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
color: #ffffff;
border-color: #7c3aed;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35);
}
.btn-export-gif:hover {
background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);
}
.btn-export-gif.recording {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border-color: #b91c1c;
animation: pulseRec 1s infinite alternate;
}
@keyframes pulseRec {
0% { transform: scale(1); }
100% { transform: scale(1.05); }
}
.btn-music-play.active-music {
background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%);
color: #ffffff;
border-color: #f43f5e;
box-shadow: 0 4px 12px rgba(244, 63, 94, 0.4);
}
.btn-gift-unwrap {
background: rgba(245, 158, 11, 0.15);
border-color: rgba(245, 158, 11, 0.3);
color: #fbbf24;
}
.btn-gift-unwrap:hover {
background: rgba(245, 158, 11, 0.25);
color: #fef08a;
}
/* Stage Layout (Grid: Code Terminal + Canvas Tree) */
.heart-stage-layout {
display: grid;
grid-template-columns: 1fr 1.25fr;
gap: 20px;
align-items: center;
}
@media (max-width: 900px) {
.heart-stage-layout {
grid-template-columns: 1fr;
}
}
/* Code Terminal Card */
.code-terminal-card {
background: #0b1329;
border: 1px solid #1e293b;
border-radius: 18px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
gap: 16px;
min-height: 420px;
}
.terminal-top-bar {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #1e293b;
padding-bottom: 12px;
}
.terminal-dots {
display: flex;
gap: 6px;
}
.t-dot {
width: 11px;
height: 11px;
border-radius: 50%;
display: inline-block;
}
.dot-red { background: #ef4444; }
.dot-yellow { background: #f59e0b; }
.dot-green { background: #10b981; }
.terminal-file-name {
font-size: 0.8rem;
font-family: monospace;
color: #94a3b8;
}
/* Typed Code Lines */
.code-editor-lines {
font-family: 'Consolas', 'Fira Code', Courier, monospace;
font-size: 0.92rem;
line-height: 1.7;
color: #e2e8f0;
min-height: 240px;
}
.code-line-item {
margin-bottom: 4px;
animation: codeLineFade 0.25s ease-out;
}
@keyframes codeLineFade {
from { opacity: 0; transform: translateX(-4px); }
to { opacity: 1; transform: translateX(0); }
}
.c-tag { color: #f43f5e; font-weight: 700; }
.c-attr { color: #38bdf8; }
.c-str { color: #fbbf24; }
.c-text { color: #f1f5f9; }
.c-highlight {
color: #f472b6;
font-weight: 700;
text-shadow: 0 0 8px rgba(244, 114, 182, 0.5);
}
.blinking-code-cursor {
color: #f43f5e;
font-weight: 900;
animation: blinkCursor 0.75s infinite;
}
@keyframes blinkCursor {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Milestone Clock Box */
.milestone-clock-card {
background: rgba(15, 23, 42, 0.7);
border: 1px solid #334155;
border-radius: 14px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 14px;
}
.clock-badge-icon {
width: 40px;
height: 40px;
background: rgba(244, 63, 94, 0.15);
border: 1px solid rgba(244, 63, 94, 0.3);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fb7185;
font-size: 1.15rem;
}
.clock-text-wrap {
display: flex;
flex-direction: column;
}
.clock-title {
font-size: 0.72rem;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.clock-display-live {
font-family: monospace;
font-size: 0.92rem;
font-weight: 800;
color: #f472b6;
}
/* Canvas Tree Container */
.canvas-wrapper-box {
background: #0b1329;
border: 1px solid #1e293b;
border-radius: 20px;
padding: 10px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
#canvas {
width: 100%;
height: auto;
max-width: 680px;
background: radial-gradient(circle at 50% 30%, #172554 0%, #0b1329 100%);
border-radius: 14px;
cursor: crosshair;
}
.canvas-interactive-toast {
position: absolute;
bottom: 16px;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(8px);
border: 1px solid rgba(244, 114, 182, 0.3);
color: #f472b6;
font-size: 0.75rem;
font-weight: 700;
padding: 4px 14px;
border-radius: 20px;
pointer-events: none;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
/* 3D Gift Modal */
.gift-modal-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(8px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.gift-modal-overlay.hidden { display: none; }
.gift-surprise-card {
background: #1e293b;
border: 1px solid #475569;
border-radius: 24px;
padding: 32px 24px;
text-align: center;
max-width: 480px;
width: 100%;
box-shadow: 0 25px 60px rgba(0,0,0,0.8), 0 0 30px rgba(244, 63, 94, 0.3);
animation: giftPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes giftPop {
0% { transform: scale(0.8); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.gift-box-3d {
font-size: 4rem;
margin-bottom: 12px;
animation: floatGift 2s infinite ease-in-out alternate;
}
@keyframes floatGift {
0% { transform: translateY(0px) rotate(-5deg); }
100% { transform: translateY(-10px) rotate(5deg); }
}
.surprise-title {
font-size: 1.6rem;
font-weight: 800;
color: #ffffff;
margin-bottom: 10px;
}
.surprise-quote {
font-size: 0.95rem;
color: #cbd5e1;
line-height: 1.6;
font-style: italic;
margin-bottom: 20px;
}
.btn-close-gift {
background: linear-gradient(135deg, #f43f5e 0%, #ec4899 100%);
color: #ffffff;
border: none;
padding: 12px 28px;
border-radius: 50px;
font-size: 1rem;
font-weight: 800;
cursor: pointer;
box-shadow: 0 4px 15px rgba(244, 63, 94, 0.4);
}
/* Wish Customization Modal */
.wish-modal-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(8px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.wish-modal-overlay.hidden { display: none; }
.wish-card-dialog {
background: #1e293b;
border: 1px solid #334155;
border-radius: 20px;
padding: 24px;
max-width: 500px;
width: 100%;
box-shadow: 0 20px 50px rgba(0,0,0,0.6);
}
.wish-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.wish-dialog-header h3 {
font-size: 1.15rem;
font-weight: 800;
color: #f8fafc;
margin: 0;
}
.btn-close-x {
background: transparent;
border: none;
color: #94a3b8;
font-size: 1.1rem;
cursor: pointer;
}
.wish-dialog-body {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 20px;
}
.wish-field label {
display: block;
font-size: 0.85rem;
font-weight: 700;
color: #cbd5e1;
margin-bottom: 6px;
}
.wish-field input,
.wish-field textarea {
width: 100%;
background: #0f172a;
border: 1px solid #334155;
border-radius: 12px;
padding: 10px 14px;
color: #f8fafc;
font-family: inherit;
font-size: 0.92rem;
outline: none;
}
.wish-field input:focus,
.wish-field textarea:focus {
border-color: #f43f5e;
box-shadow: 0 0 0 3px rgba(244, 63, 94, 0.2);
}
.wish-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn-wish-cancel {
background: #334155;
border: none;
color: #cbd5e1;
padding: 10px 18px;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
}
.btn-wish-submit {
background: linear-gradient(135deg, #f43f5e 0%, #ec4899 100%);
border: none;
color: #ffffff;
padding: 10px 20px;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 4px 12px rgba(244, 63, 94, 0.4);
}C Step 3: JavaScript Logic & Event Handling
Here is the complete JavaScript code handling the state, event listeners, mathematical logic, and dynamic DOM rendering:
/**
* Special Birthday Code Surprise Engine
* Procedural Fractal Tree, Blooming Heart Canopy, Terminal Typewriter, Music Box & GIF Export
*/
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// State Variables
let treeBranches = [];
let heartBlossoms = [];
let fallingLeaves = [];
let cursorHearts = [];
let celebrationConfetti = [];
let isTreeComplete = false;
let animFrame = null;
let celebratedSeconds = 365 * 86400 + 14 * 3600 + 28 * 60;
let isRecording = false;
// Typewriter Config
let recipient = "My Favorite Person";
let codeLines = [
'<span class="c-tag"><div</span> <span class="c-attr">id</span>=<span class="c-str">"code"</span><span class="c-tag">></span>',
' <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>Hey you π<span class="c-tag"></span><br /></span>',
' <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>Happy Birthday πβ¨<span class="c-tag"></span><br /></span>',
' <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>May God bless you πΈ<span class="c-tag"></span><br /></span>',
' <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>And give u many happiness π<span class="c-tag"></span><br /></span>',
' <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>Just saying... you\'re pretty awesome π<span class="c-tag"></span><br /></span>',
' <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>Sending good vibes and maybe a wink π<span class="c-tag"></span><br /></span>',
' <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>Hope u have a great day today π<span class="c-tag"></span><br /></span>',
' <span class="c-highlight">// I turned HTML, CSS, and JS into a special birthday gift. β¨</span>',
'<span class="c-tag"></div></span>'
];
let activeLineIdx = 0;
let activeCharIdx = 0;
let typewriterTimer = null;
// Palette for Heart Canopy
const PETAL_COLORS = [
'#ff4d6d', '#f72585', '#ff758f', '#ff85a1',
'#ff99c8', '#ffb703', '#fb8500', '#f15bb5',
'#fee440', '#e63946', '#ffb3c1', '#f43f5e'
];
/**
* 1. Parametric Heart Formula
*/
function getHeartCoord(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 };
}
function drawHeartShape(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. Initialize Tree & Heart Blossom Coordinates
*/
function buildTreeSystem() {
treeBranches = [];
heartBlossoms = [];
fallingLeaves = [];
isTreeComplete = false;
const rootX = canvas.width * 0.5;
const rootY = canvas.height - 20;
const heartCenterY = canvas.height * 0.38;
const heartCenterX = canvas.width * 0.5;
// Build Procedural Branches
createBranch(rootX, rootY, -Math.PI / 2, 118, 14, 0);
// Generate 480 Heart-shaped Blossoms
for (let i = 0; i < 480; i++) {
const t = Math.random() * Math.PI * 2;
const r = Math.sqrt(Math.random());
const pt = getHeartCoord(t, 12.8);
const targetX = heartCenterX + pt.x * r + (Math.random() * 8 - 4);
const targetY = heartCenterY + pt.y * r + (Math.random() * 8 - 4);
heartBlossoms.push({
x: targetX,
y: targetY,
size: Math.random() * 7 + 7,
color: PETAL_COLORS[Math.floor(Math.random() * PETAL_COLORS.length)],
angle: (Math.random() - 0.5) * 0.8,
delay: Math.random() * 80 + 25,
progress: 0,
scale: 0
});
}
}
function createBranch(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,
current: 0,
target: 1
});
if (depth >= 4 || length < 25) return;
const subCount = depth === 0 ? 3 : (Math.random() > 0.35 ? 2 : 3);
for (let i = 0; i < subCount; i++) {
const spread = (i - (subCount - 1) / 2) * 0.42 + (Math.random() * 0.16 - 0.08);
const newLen = length * (0.66 + Math.random() * 0.14);
const newWidth = Math.max(width * 0.65, 2.5);
createBranch(endX, endY, angle + spread, newLen, newWidth, depth + 1);
}
}
/**
* 3. Render Animation Loop
*/
function renderFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Base Mound / Tree Soil
ctx.fillStyle = 'rgba(244, 114, 182, 0.2)';
ctx.beginPath();
ctx.ellipse(canvas.width * 0.5, canvas.height - 12, 120, 16, 0, 0, Math.PI * 2);
ctx.fill();
// Draw Branches
let allDone = true;
treeBranches.forEach(b => {
if (b.current < b.target) {
b.current += 0.045;
if (b.current > b.target) b.current = b.target;
allDone = false;
}
const curX = b.startX + (b.endX - b.startX) * b.current;
const curY = b.startY + (b.endY - b.startY) * b.current;
ctx.save();
ctx.strokeStyle = '#c08497';
ctx.lineWidth = Math.max(b.width * (1 - b.depth * 0.15), 2.2);
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(b.startX, b.startY);
ctx.lineTo(curX, curY);
ctx.stroke();
ctx.restore();
});
if (allDone) {
isTreeComplete = true;
}
// Draw Heart Blossoms
if (isTreeComplete) {
heartBlossoms.forEach(b => {
if (b.delay > 0) {
b.delay--;
} else if (b.progress < 1) {
b.progress += 0.05;
if (b.progress > 1) b.progress = 1;
b.scale = (1 - Math.cos(b.progress * Math.PI)) * 0.5;
}
if (b.scale > 0) {
drawHeartShape(ctx, b.x, b.y, b.size * b.scale, b.color, b.angle);
}
});
// Spawn falling leaves
if (Math.random() < 0.28) {
const randB = heartBlossoms[Math.floor(Math.random() * heartBlossoms.length)];
if (randB && randB.scale >= 0.8) {
fallingLeaves.push({
x: randB.x,
y: randB.y,
size: Math.random() * 5 + 6,
color: randB.color,
vx: (Math.random() - 0.5) * 1.6,
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 Falling Leaves
for (let i = fallingLeaves.length - 1; i >= 0; i--) {
const p = fallingLeaves[i];
p.sway += 0.035;
p.x += p.vx + Math.sin(p.sway) * 0.85;
p.y += p.vy;
p.angle += p.vAngle;
if (p.y > canvas.height - 20) p.alpha -= 0.02;
drawHeartShape(ctx, p.x, p.y, p.size, p.color, p.angle, p.alpha);
if (p.alpha <= 0 || p.y > canvas.height + 20) {
fallingLeaves.splice(i, 1);
}
}
// Cursor Trail Hearts
for (let i = cursorHearts.length - 1; i >= 0; i--) {
const h = cursorHearts[i];
h.x += h.vx;
h.y += h.vy;
h.vy -= 0.06;
h.alpha -= 0.018;
h.size *= 0.99;
drawHeartShape(ctx, h.x, h.y, h.size, h.color, h.angle, h.alpha);
if (h.alpha <= 0) {
cursorHearts.splice(i, 1);
}
}
// Celebration Confetti
for (let i = celebrationConfetti.length - 1; i >= 0; i--) {
const c = celebrationConfetti[i];
c.x += c.vx;
c.y += c.vy;
c.vy += 0.22;
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.w / 2, -c.h / 2, c.w, c.h);
ctx.restore();
if (c.life <= 0 || c.y > canvas.height + 20) {
celebrationConfetti.splice(i, 1);
}
}
animFrame = requestAnimationFrame(renderFrame);
}
/**
* 4. Terminal Typewriter
*/
function startTerminalTypewriter() {
clearTimeout(typewriterTimer);
const block = document.getElementById('typedCodeBlock');
block.innerHTML = '';
activeLineIdx = 0;
typewriterLineLoop();
}
function typewriterLineLoop() {
if (activeLineIdx >= codeLines.length) return;
const block = document.getElementById('typedCodeBlock');
const div = document.createElement('div');
div.className = 'code-line-item';
div.innerHTML = codeLines[activeLineIdx];
block.appendChild(div);
activeLineIdx++;
typewriterTimer = setTimeout(typewriterLineLoop, 280);
}
/**
* 5. Milestone Clock
*/
function updateClock() {
celebratedSeconds++;
const days = Math.floor(celebratedSeconds / 86400);
const hrs = Math.floor((celebratedSeconds % 86400) / 3600);
const mins = Math.floor((celebratedSeconds % 3600) / 60);
const secs = celebratedSeconds % 60;
const clockEl = document.getElementById('clock');
if (clockEl) {
clockEl.textContent = `${days} days ${hrs} hours ${mins} minutes ${secs} seconds`;
}
}
setInterval(updateClock, 1000);
/**
* 6. Web Audio API Acoustic Birthday Chime
*/
let audioContext = null;
let isAudioPlaying = false;
let noteTimer = null;
let noteIndex = 0;
function getAudio() {
if (!audioContext) {
const Ctor = window.AudioContext || window.webkitAudioContext;
if (Ctor) audioContext = new Ctor();
}
if (audioContext && audioContext.state === 'suspended') {
audioContext.resume();
}
return audioContext;
}
const SONG_NOTES = [
{ f: 261.63, d: 350 }, { f: 261.63, d: 250 }, { f: 293.66, d: 500 }, { f: 261.63, d: 500 },
{ f: 349.23, d: 500 }, { f: 329.63, d: 900 }, { f: 0, d: 250 },
{ f: 261.63, d: 350 }, { f: 261.63, d: 250 }, { f: 293.66, d: 500 }, { f: 261.63, d: 500 },
{ f: 392.00, d: 500 }, { f: 349.23, d: 900 }, { f: 0, d: 250 },
{ f: 261.63, d: 350 }, { f: 261.63, d: 250 }, { f: 523.25, d: 500 }, { f: 440.00, d: 500 },
{ f: 349.23, d: 500 }, { f: 329.63, d: 500 }, { f: 293.66, d: 700 }, { f: 0, d: 250 },
{ f: 466.16, d: 350 }, { f: 466.16, d: 250 }, { f: 440.00, d: 500 }, { f: 349.23, d: 500 },
{ f: 392.00, d: 500 }, { f: 349.23, d: 1200}
];
function playNextNote() {
if (!isAudioPlaying) return;
const ctx = getAudio();
if (!ctx) return;
const n = SONG_NOTES[noteIndex];
if (n.f > 0) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(n.f, ctx.currentTime);
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + (n.d / 1000));
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + (n.d / 1000));
}
noteIndex = (noteIndex + 1) % SONG_NOTES.length;
noteTimer = setTimeout(playNextNote, n.d + 60);
}
function toggleAudio() {
const btn = document.getElementById('btnAudioMelody');
const txt = document.getElementById('audioStatusText');
if (!isAudioPlaying) {
isAudioPlaying = true;
btn.classList.add('active-music');
txt.textContent = 'Pause Music';
noteIndex = 0;
playNextNote();
} else {
isAudioPlaying = false;
clearTimeout(noteTimer);
btn.classList.remove('active-music');
txt.textContent = 'Play Music';
}
}
/**
* 7. Interactive Cursor Trail & Click Fireworks
*/
canvas.addEventListener('mousemove', (e) => {
if (Math.random() > 0.4) return;
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;
cursorHearts.push({
x: x,
y: y,
size: Math.random() * 6 + 6,
color: PETAL_COLORS[Math.floor(Math.random() * PETAL_COLORS.length)],
vx: (Math.random() - 0.5) * 2,
vy: -Math.random() * 2,
angle: (Math.random() - 0.5) * 0.5,
alpha: 0.9
});
});
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 < 16; i++) {
cursorHearts.push({
x: x,
y: y,
size: Math.random() * 10 + 10,
color: PETAL_COLORS[Math.floor(Math.random() * PETAL_COLORS.length)],
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8 - 3,
angle: (Math.random() - 0.5) * 0.8,
alpha: 1
});
}
});
/**
* 8. Confetti Cannon Burst
*/
function fireConfetti() {
const cols = ['#f43f5e', '#ec4899', '#facc15', '#38bdf8', '#a855f7', '#10b981'];
for (let i = 0; i < 100; i++) {
celebrationConfetti.push({
x: canvas.width * 0.5 + (Math.random() * 100 - 50),
y: canvas.height * 0.35,
vx: (Math.random() - 0.5) * 16,
vy: (Math.random() - 0.7) * 15,
w: Math.random() * 8 + 6,
h: Math.random() * 6 + 4,
color: cols[Math.floor(Math.random() * cols.length)],
rotation: Math.random() * Math.PI * 2,
vRot: (Math.random() - 0.5) * 0.25,
life: Math.random() * 80 + 90
});
}
}
/**
* 9. Record and Download Animated GIF
*/
function recordAndDownloadCardGif() {
if (isRecording) return;
const btn = document.getElementById('btnRecordCardGif');
const txt = document.getElementById('gifExportText');
isRecording = true;
btn.classList.add('recording');
btn.disabled = true;
const frames = [];
const total = 28;
let current = 0;
const offscreen = document.createElement('canvas');
offscreen.width = 540;
offscreen.height = 460;
const octx = offscreen.getContext('2d');
const interval = setInterval(() => {
current++;
txt.textContent = `Recording ${current}/${total}...`;
octx.fillStyle = '#0b1329';
octx.fillRect(0, 0, offscreen.width, offscreen.height);
octx.drawImage(canvas, 0, 0, offscreen.width, offscreen.height);
octx.fillStyle = '#f472b6';
octx.font = 'bold 14px Consolas, monospace';
octx.textAlign = 'center';
octx.fillText(`Happy Birthday, ${recipient}! β€οΈβ¨`, offscreen.width / 2, offscreen.height - 14);
frames.push(offscreen.toDataURL('image/jpeg', 0.85));
if (current >= total) {
clearInterval(interval);
txt.textContent = 'Rendering GIF...';
if (typeof gifshot !== 'undefined') {
gifshot.createGIF({
images: frames,
gifWidth: 540,
gifHeight: 460,
interval: 0.1,
numFrames: total,
sampleInterval: 10
}, function(obj) {
isRecording = false;
btn.classList.remove('recording');
btn.disabled = false;
if (!obj.error) {
const link = document.createElement('a');
link.href = obj.image;
const safe = recipient.toLowerCase().replace(/[^a-z0-9]/g, '-');
link.download = `special-birthday-gift-${safe}.gif`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
txt.textContent = 'GIF Saved! π';
setTimeout(() => { txt.textContent = 'Download GIF'; }, 3500);
} else {
fallbackDownload();
}
});
} else {
fallbackDownload();
}
}
}, 100);
function fallbackDownload() {
isRecording = false;
btn.classList.remove('recording');
btn.disabled = false;
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
const safe = recipient.toLowerCase().replace(/[^a-z0-9]/g, '-');
link.download = `special-birthday-gift-${safe}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
txt.textContent = 'Saved Snapshot!';
setTimeout(() => { txt.textContent = 'Download GIF'; }, 3500);
}
}
/**
* 10. Modals and Triggers
*/
document.getElementById('btnAudioMelody').addEventListener('click', toggleAudio);
document.getElementById('btnRecordCardGif').addEventListener('click', recordAndDownloadCardGif);
document.getElementById('btnReplaySurprise').addEventListener('click', () => {
buildTreeSystem();
startTerminalTypewriter();
fireConfetti();
});
// Gift Modal
const giftModal = document.getElementById('giftModal');
document.getElementById('btnUnwrapGift').addEventListener('click', () => {
giftModal.classList.remove('hidden');
fireConfetti();
});
document.getElementById('btnCloseGiftModal').addEventListener('click', () => {
giftModal.classList.add('hidden');
});
// Wish Edit Modal
const wishModal = document.getElementById('wishModal');
document.getElementById('btnOpenWishModal').addEventListener('click', () => {
wishModal.classList.remove('hidden');
});
document.getElementById('btnCloseWishModal').addEventListener('click', () => {
wishModal.classList.add('hidden');
});
document.getElementById('btnCancelWishModal').addEventListener('click', () => {
wishModal.classList.add('hidden');
});
document.getElementById('btnSaveWishModal').addEventListener('click', () => {
const name = document.getElementById('recipientInput').value.trim();
const rawPoem = document.getElementById('poemInput').value.trim();
if (name) {
recipient = name;
document.getElementById('dedicationTag').textContent = `To: ${recipient} π`;
}
if (rawPoem) {
const rawLines = rawPoem.split('\n').map(l => l.trim()).filter(l => l.length > 0);
codeLines = [
'<span class="c-tag"><div</span> <span class="c-attr">id</span>=<span class="c-str">"code"</span><span class="c-tag">></span>',
...rawLines.map(l => ` <span class="c-tag"><span</span> <span class="c-attr">class</span>=<span class="c-str">"say"</span><span class="c-tag">></span>${l}<span class="c-tag"></span><br /></span>`),
'<span class="c-tag"></div></span>'
];
}
wishModal.classList.add('hidden');
buildTreeSystem();
startTerminalTypewriter();
fireConfetti();
});
// Boot Initial
buildTreeSystem();
startTerminalTypewriter();
renderFrame();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.
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
special-birthday-gift-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.