1. Project Overview & Features
The 3D 'Add to Cart' Product Delivery Truck & Box 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:
- Starts with an interactive 3D 'Add to Cart π' teal button floating in center stage
- 3D button morphs into a Kraft cardboard parcel box with 4 opening hinged flaps (matching Image 3)
- Smooth camera ease & scene pan revealing the yellow delivery truck parked with open cargo doors
- Procedural Kraft textures with β» recycling, πΈ fragile glass, and barcode serial stamps
- Parabolic quadratic bezier trajectory curve with 3D directional arrow loading into the truck
- Realistic truck cargo bay impact with suspension squash and spring damping bounce
- Rear double cargo doors swing shut and latch securely before truck accelerates off-screen
- Multi-speed playback controller (0.5x Slow-Motion, 1.0x Normal, 1.5x Fast)
- 4 Selectable e-commerce products (Studio Headphones, Smartwatch, 4K Drone, Keyboard)
- Web Audio API sound synthesizers for button clicks, unboxing rustle, flight whoosh, cargo impact, latching, and engine revs
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="cart-truck-app" id="cartTruckApp">
<!-- Three.js 3D WebGL Canvas -->
<canvas id="threeCanvas" class="three-canvas"></canvas>
<!-- Confetti Canvas for Delivery Celebration -->
<canvas id="confettiCanvas" class="confetti-canvas"></canvas>
<!-- Top Studio Header -->
<header class="app-header">
<div class="header-left">
<div class="badge-pill"><i class="fas fa-truck-fast text-amber"></i> Express E-Commerce Logistics</div>
<h1 class="header-title">3D ‘Add to Cart’ Delivery Experience</h1>
</div>
<div class="header-right">
<!-- Speed Controller -->
<div class="speed-control-group" title="Animation Playback Speed">
<button type="button" class="speed-btn" data-speed="0.5">0.5x</button>
<button type="button" class="speed-btn active" data-speed="1.0">1.0x</button>
<button type="button" class="speed-btn" data-speed="1.5">1.5x</button>
</div>
<!-- Floating Utility Buttons -->
<button type="button" class="dock-btn" id="btnAudioToggle" title="Toggle Sound FX">
<i class="fas fa-volume-up"></i>
</button>
<button type="button" class="dock-btn" id="btnThemeToggle" title="Switch Studio Background Theme">
<i class="fas fa-palette"></i>
</button>
<button type="button" class="dock-btn" id="btnProductModal" title="Select Product">
<i class="fas fa-box-open"></i>
</button>
</div>
</header>
<!-- Interactive 3D Product & Add to Cart Stage (Bottom Hint & Controls) -->
<div class="cart-action-stage" id="cartActionStage">
<div class="product-preview-card">
<div class="product-meta-row">
<span class="product-category-tag" id="dispProductCat"><i class="fas fa-headphones me-1"></i> Premium Audio</span>
<span class="product-stock-tag"><i class="fas fa-check-circle me-1"></i> In Stock • Express 24h</span>
</div>
<h2 class="product-main-title" id="dispProductName">Studio Pro Wireless ANC Headphones</h2>
<div class="product-price-row">
<span class="product-price" id="dispProductPrice">$299.00</span>
<span class="product-original-price">$349.00</span>
<span class="product-discount-badge">-15% OFF</span>
</div>
<p class="product-desc" id="dispProductDesc">Click the 3D <strong>“Add to Cart π”</strong> button on screen or below to watch it morph into a parcel box and load onto the delivery van!</p>
<!-- Interactive Add to Cart Button -->
<button type="button" class="btn-add-to-cart" id="btnAddCart">
<div class="btn-cart-content">
<div class="cart-icon-wrap">
<i class="fas fa-cart-shopping"></i>
</div>
<span class="btn-label" id="btnCartLabel">Add to Cart • Ship Now</span>
</div>
<div class="btn-cart-glow"></div>
</button>
<div class="shipping-perk-hint">
<i class="fas fa-mouse-pointer text-amber me-1"></i> <strong>Interactive:</strong> Click either the 3D button above or the button here!
</div>
</div>
</div>
<!-- Live Logistics Stepper Overlay -->
<div class="logistics-stepper-overlay hidden" id="statusStepper">
<div class="stepper-glass-container">
<div class="stepper-header">
<span class="pulse-indicator"></span>
<span class="stepper-title" id="stepperStatusText">Morphing & Unfolding Cardboard Parcel Box...</span>
</div>
<div class="stepper-track">
<div class="step-item active" id="step1">
<div class="step-dot"><i class="fas fa-box-open"></i></div>
<span class="step-name">1. Box Fold</span>
</div>
<div class="step-line" id="line1"></div>
<div class="step-item" id="step2">
<div class="step-dot"><i class="fas fa-arrow-up-right-dots"></i></div>
<span class="step-name">2. Trajectory Load</span>
</div>
<div class="step-line" id="line2"></div>
<div class="step-item" id="step3">
<div class="step-dot"><i class="fas fa-lock"></i></div>
<span class="step-name">3. Cargo Latched</span>
</div>
<div class="step-line" id="line3"></div>
<div class="step-item" id="step4">
<div class="step-dot"><i class="fas fa-truck-fast"></i></div>
<span class="step-name">4. Out for Delivery</span>
</div>
</div>
</div>
</div>
<!-- Delivery Success Modal Card -->
<div class="delivery-receipt-modal hidden" id="receiptModal">
<div class="receipt-card">
<div class="receipt-badge-success">
<i class="fas fa-truck-ramp-box"></i>
</div>
<h3 class="receipt-title">Dispatched & Out for Delivery!</h3>
<p class="receipt-subtitle">Your package was loaded onto delivery van #24 and is speeding toward you.</p>
<div class="receipt-details-grid">
<div class="receipt-row">
<span class="row-label">Item</span>
<strong class="row-val" id="receiptItemName">Studio Pro Wireless ANC Headphones</strong>
</div>
<div class="receipt-row">
<span class="row-label">Tracking #</span>
<strong class="row-val tracking-code">TRK-8942-EXP-2026</strong>
</div>
<div class="receipt-row">
<span class="row-label">Carrier</span>
<strong class="row-val">Apex Express Logistics Van</strong>
</div>
<div class="receipt-row">
<span class="row-label">Estimated Arrival</span>
<strong class="row-val text-emerald">Tomorrow by 10:00 AM</strong>
</div>
</div>
<div class="receipt-actions">
<button type="button" class="btn-replay" id="btnReplayDelivery">
<i class="fas fa-rotate-left me-1"></i> Replay Animation
</button>
<button type="button" class="btn-reset-view" id="btnResetView">
<i class="fas fa-arrow-left me-1"></i> Back to Start
</button>
</div>
</div>
</div>
<!-- Product Switcher Modal -->
<div class="custom-modal-overlay hidden" id="productModal">
<div class="custom-modal-dialog">
<div class="modal-head">
<h4><i class="fas fa-boxes-stacked text-amber me-2"></i> Select Product to Pack & Ship</h4>
<button type="button" class="btn-close-modal" id="btnCloseModal">×</button>
</div>
<div class="product-grid-selector">
<div class="product-option-card active" data-product="0">
<div class="opt-icon"><i class="fas fa-headphones"></i></div>
<div class="opt-info">
<h5>Studio Pro ANC Headphones</h5>
<span>$299.00 • Audio</span>
</div>
</div>
<div class="product-option-card" data-product="1">
<div class="opt-icon"><i class="fas fa-clock"></i></div>
<div class="opt-info">
<h5>Titanium Smartwatch Ultra</h5>
<span>$399.00 • Wearables</span>
</div>
</div>
<div class="product-option-card" data-product="2">
<div class="opt-icon"><i class="fas fa-helicopter"></i></div>
<div class="opt-info">
<h5>4K Cinematic Pro Drone</h5>
<span>$749.00 • Cameras</span>
</div>
</div>
<div class="product-option-card" data-product="3">
<div class="opt-icon"><i class="fas fa-keyboard"></i></div>
<div class="opt-info">
<h5>Mechanical Pro Keyboard</h5>
<span>$189.00 • Peripherals</span>
</div>
</div>
</div>
<div class="modal-foot">
<button type="button" class="btn-modal-primary" id="btnSelectProductDone">Select Product</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.
/* 3D Add to Cart Product Delivery Studio Styling */
:root {
--bg-studio: #bdd6bd; /* Sage Green matching viral screenshot */
--card-bg: rgba(255, 255, 255, 0.88);
--card-border: rgba(255, 255, 255, 0.95);
--text-main: #0f172a;
--text-muted: #64748b;
--amber-primary: #f59e0b;
--amber-dark: #d97706;
--teal-btn: #00766c;
--emerald-accent: #10b981;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
}
.theme-sunset {
--bg-studio: #f5ebe0;
--card-bg: rgba(255, 255, 255, 0.9);
}
.theme-slate {
--bg-studio: #cbd5e1;
--card-bg: rgba(255, 255, 255, 0.92);
}
.theme-midnight {
--bg-studio: #0f172a;
--card-bg: rgba(30, 41, 59, 0.92);
--card-border: rgba(51, 65, 85, 0.8);
--text-main: #f8fafc;
--text-muted: #94a3b8;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cart-truck-app {
position: relative;
width: 100%;
height: 100vh;
min-height: 600px;
background-color: var(--bg-studio);
font-family: var(--font-sans);
color: var(--text-main);
overflow: hidden;
user-select: none;
transition: background-color 0.5s ease;
}
/* Three.js Canvas */
.three-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
cursor: grab;
}
.three-canvas:active {
cursor: grabbing;
}
/* Confetti Canvas */
.confetti-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none;
}
/* Top App Header */
.app-header {
position: absolute;
top: 18px;
left: 24px;
right: 24px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 15;
pointer-events: none;
}
.header-left, .header-right {
pointer-events: auto;
display: flex;
align-items: center;
gap: 12px;
}
.badge-pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 5px 12px;
border-radius: 9999px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #334155;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 255, 255, 0.8);
}
.badge-pill .text-amber {
color: var(--amber-dark);
}
.header-title {
font-size: 1.15rem;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.02em;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
}
/* Speed Controls */
.speed-control-group {
display: flex;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 12px;
padding: 3px;
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.speed-btn {
background: transparent;
border: none;
outline: none;
padding: 5px 10px;
font-size: 0.78rem;
font-weight: 700;
color: var(--text-muted);
border-radius: 9px;
cursor: pointer;
transition: all 0.2s ease;
}
.speed-btn:hover {
color: var(--text-main);
}
.speed-btn.active {
background: var(--amber-primary);
color: #ffffff;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.35);
}
/* Dock Buttons */
.dock-btn {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.9);
color: var(--text-main);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
cursor: pointer;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dock-btn:hover {
transform: translateY(-2px);
background: #ffffff;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
color: var(--amber-dark);
}
.dock-btn:active {
transform: scale(0.94);
}
/* Center Bottom Product Card Stage */
.cart-action-stage {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
width: 90%;
max-width: 480px;
z-index: 12;
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.cart-action-stage.hide-stage {
opacity: 0;
transform: translate(-50%, 40px);
pointer-events: none;
}
.product-preview-card {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 20px 24px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.04);
}
.product-meta-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.product-category-tag {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #0284c7;
background: rgba(2, 132, 199, 0.1);
padding: 3px 8px;
border-radius: 6px;
}
.product-stock-tag {
font-size: 0.75rem;
font-weight: 600;
color: var(--emerald-accent);
}
.product-main-title {
font-size: 1.22rem;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.02em;
margin-bottom: 6px;
}
.product-price-row {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 8px;
}
.product-price {
font-size: 1.4rem;
font-weight: 900;
color: #047857;
}
.product-original-price {
font-size: 0.95rem;
color: var(--text-muted);
text-decoration: line-through;
}
.product-discount-badge {
font-size: 0.72rem;
font-weight: 800;
background: #fee2e2;
color: #ef4444;
padding: 2px 6px;
border-radius: 5px;
}
.product-desc {
font-size: 0.82rem;
line-height: 1.45;
color: var(--text-muted);
margin-bottom: 14px;
}
/* Big 3D Add to Cart Button (Bottom UI) */
.btn-add-to-cart {
position: relative;
width: 100%;
padding: 14px 20px;
border-radius: 14px;
border: none;
outline: none;
background: linear-gradient(135deg, #00766c 0%, #005951 100%);
color: #ffffff;
font-size: 1.05rem;
font-weight: 800;
cursor: pointer;
box-shadow: 0 8px 24px rgba(0, 118, 108, 0.4), 0 3px 0 #003d37;
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
}
.btn-add-to-cart:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgba(0, 118, 108, 0.48), 0 3px 0 #003d37;
}
.btn-add-to-cart:active {
transform: translateY(2px);
box-shadow: 0 4px 12px rgba(0, 118, 108, 0.3), 0 1px 0 #003d37;
}
.btn-cart-content {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
position: relative;
z-index: 2;
}
.cart-icon-wrap {
width: 32px;
height: 32px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.22);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
.shipping-perk-hint {
font-size: 0.76rem;
color: var(--text-muted);
text-align: center;
margin-top: 10px;
}
/* Stepper Overlay */
.logistics-stepper-overlay {
position: absolute;
top: 76px;
left: 50%;
transform: translateX(-50%);
z-index: 14;
width: 90%;
max-width: 580px;
transition: all 0.4s ease;
}
.logistics-stepper-overlay.hidden {
opacity: 0;
transform: translate(-50%, -20px);
pointer-events: none;
}
.stepper-glass-container {
background: rgba(255, 255, 255, 0.88);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.95);
border-radius: 18px;
padding: 14px 20px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
}
.stepper-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.pulse-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
animation: pulseGreen 1.5s infinite;
}
@keyframes pulseGreen {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 8px rgba(16, 185, 129, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
.stepper-title {
font-size: 0.88rem;
font-weight: 800;
color: var(--text-main);
}
.stepper-track {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
z-index: 2;
}
.step-dot {
width: 32px;
height: 32px;
border-radius: 50%;
background: #e2e8f0;
color: #94a3b8;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 700;
transition: all 0.3s ease;
}
.step-name {
font-size: 0.7rem;
font-weight: 700;
color: var(--text-muted);
white-space: nowrap;
}
.step-item.active .step-dot {
background: var(--amber-primary);
color: #ffffff;
box-shadow: 0 0 12px rgba(245, 158, 11, 0.45);
transform: scale(1.1);
}
.step-item.active .step-name {
color: var(--text-main);
font-weight: 800;
}
.step-line {
flex: 1;
height: 3px;
background: #e2e8f0;
margin: 0 6px 18px 6px;
transition: background 0.4s ease;
}
.step-line.active {
background: var(--amber-primary);
}
/* Delivery Receipt Modal */
.delivery-receipt-modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 440px;
z-index: 20;
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.delivery-receipt-modal.hidden {
opacity: 0;
transform: translate(-50%, -40%) scale(0.9);
pointer-events: none;
}
.receipt-card {
background: var(--card-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--card-border);
border-radius: 24px;
padding: 28px 24px;
text-align: center;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.15);
}
.receipt-badge-success {
width: 56px;
height: 56px;
border-radius: 18px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: #ffffff;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px auto;
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.35);
}
.receipt-title {
font-size: 1.28rem;
font-weight: 800;
color: var(--text-main);
margin-bottom: 6px;
}
.receipt-subtitle {
font-size: 0.82rem;
color: var(--text-muted);
line-height: 1.45;
margin-bottom: 20px;
}
.receipt-details-grid {
background: rgba(0, 0, 0, 0.03);
border-radius: 16px;
padding: 14px 16px;
margin-bottom: 22px;
display: flex;
flex-direction: column;
gap: 10px;
text-align: left;
}
.receipt-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.82rem;
}
.row-label {
color: var(--text-muted);
}
.row-val {
color: var(--text-main);
}
.tracking-code {
font-family: monospace;
background: rgba(245, 158, 11, 0.15);
color: #b45309;
padding: 2px 6px;
border-radius: 4px;
font-weight: 700;
}
.text-emerald {
color: #059669 !important;
}
.receipt-actions {
display: flex;
gap: 10px;
}
.btn-replay, .btn-reset-view {
flex: 1;
padding: 12px;
border-radius: 12px;
font-size: 0.88rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-replay {
background: var(--amber-primary);
color: #ffffff;
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35);
}
.btn-replay:hover {
background: var(--amber-dark);
transform: translateY(-1px);
}
.btn-reset-view {
background: rgba(0, 0, 0, 0.06);
color: var(--text-main);
}
.btn-reset-view:hover {
background: rgba(0, 0, 0, 0.1);
}
/* Product Selection Modal */
.custom-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.custom-modal-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.custom-modal-dialog {
background: #ffffff;
border-radius: 20px;
width: 90%;
max-width: 440px;
padding: 22px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
}
.modal-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-head h4 {
font-size: 1.05rem;
font-weight: 800;
color: #0f172a;
}
.btn-close-modal {
background: transparent;
border: none;
font-size: 1.4rem;
color: #64748b;
cursor: pointer;
}
.product-grid-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 18px;
}
.product-option-card {
border: 2px solid #e2e8f0;
border-radius: 14px;
padding: 14px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.product-option-card:hover {
border-color: #cbd5e1;
transform: translateY(-2px);
}
.product-option-card.active {
border-color: var(--amber-primary);
background: rgba(245, 158, 11, 0.06);
}
.opt-icon {
font-size: 1.4rem;
color: var(--amber-primary);
margin-bottom: 8px;
}
.opt-info h5 {
font-size: 0.82rem;
font-weight: 700;
color: #0f172a;
margin-bottom: 2px;
}
.opt-info span {
font-size: 0.72rem;
color: #64748b;
}
.btn-modal-primary {
width: 100%;
padding: 12px;
border-radius: 12px;
background: var(--amber-primary);
color: #ffffff;
font-weight: 800;
border: none;
cursor: pointer;
}
@media (max-width: 640px) {
.app-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.cart-action-stage {
width: 94%;
bottom: 14px;
}
.product-preview-card {
padding: 16px;
}
.product-grid-selector {
grid-template-columns: 1fr;
}
}C Step 3: JavaScript Logic & Event Handling
Here is the complete JavaScript code handling the state, event listeners, mathematical logic, and dynamic DOM rendering:
(() => {
// DOM Elements
const app = document.getElementById('cartTruckApp');
const threeCanvas = document.getElementById('threeCanvas');
const confettiCanvas = document.getElementById('confettiCanvas');
const cartActionStage = document.getElementById('cartActionStage');
const btnAddCart = document.getElementById('btnAddCart');
const statusStepper = document.getElementById('statusStepper');
const stepperStatusText = document.getElementById('stepperStatusText');
const receiptModal = document.getElementById('receiptModal');
const receiptItemName = document.getElementById('receiptItemName');
const btnReplayDelivery = document.getElementById('btnReplayDelivery');
const btnResetView = document.getElementById('btnResetView');
const btnAudioToggle = document.getElementById('btnAudioToggle');
const btnThemeToggle = document.getElementById('btnThemeToggle');
const btnProductModal = document.getElementById('btnProductModal');
const productModal = document.getElementById('productModal');
const btnCloseModal = document.getElementById('btnCloseModal');
const btnSelectProductDone = document.getElementById('btnSelectProductDone');
const speedBtns = document.querySelectorAll('.speed-btn');
const dispProductCat = document.getElementById('dispProductCat');
const dispProductName = document.getElementById('dispProductName');
const dispProductPrice = document.getElementById('dispProductPrice');
const dispProductDesc = document.getElementById('dispProductDesc');
// Product Catalog
const products = [
{
name: 'Studio Pro Wireless ANC Headphones',
cat: 'Premium Audio',
icon: 'fa-headphones',
price: '$299.00',
orig: '$349.00',
desc: 'Audiophile-grade noise cancellation, spatial acoustic transducers, and ultra-plush memory foam ear cushions.'
},
{
name: 'Titanium Smartwatch Ultra',
cat: 'Wearables',
icon: 'fa-clock',
price: '$399.00',
orig: '$449.00',
desc: 'Aerospace-grade titanium casing, dual-frequency GPS, 100m water resistance, and 7-day battery life.'
},
{
name: '4K Cinematic Pro Drone',
cat: 'Cameras',
icon: 'fa-helicopter',
price: '$749.00',
orig: '$899.00',
desc: '3-axis gimbal stabilized 4K HDR camera, omnidirectional obstacle sensing, and 45-minute flight time.'
},
{
name: 'Mechanical Pro Ergonomic Keyboard',
cat: 'Peripherals',
icon: 'fa-keyboard',
price: '$189.00',
orig: '$229.00',
desc: 'Hot-swappable linear switches, aircraft aluminum top plate, and customizable RGB per-key backlighting.'
}
];
// State
let currentProductIdx = 0;
let animSpeed = 1.0;
let isAnimating = false;
let isSceneInWideView = false;
let audioEnabled = true;
let audioCtx = null;
let themeIndex = 0;
const themeClasses = ['', 'theme-sunset', 'theme-slate', 'theme-midnight'];
const themeBgHex = [0xbdd6bd, 0xf5ebe0, 0xcbd5e1, 0x0f172a];
// --- 1. Three.js Scene & Objects ---
let scene, camera, renderer;
let raycaster, mouse;
let btn3DGroup, btn3DMesh;
let outerBoxGroup, boxFlaps = {};
let parcelGroup;
let truckGroup, truckBodyGroup;
let truckDoorLeft, truckDoorRight;
let wheels = [];
let exhaustParticles = [];
let trajectoryGroup = null;
let groundPlane;
// Camera Positions (Close Focus on Button vs Wide Focus on Box & Truck)
const camPosClose = { x: 0, y: 1.2, z: 9.2 };
const camPosWide = { x: 0, y: 1.8, z: 14.8 };
let currentCamPos = { ...camPosClose };
let targetCamPos = { ...camPosClose };
// Parallax & Orbit Interaction
let targetRotX = 0, targetRotY = 0;
let currentRotX = 0, currentRotY = 0;
let isDragging = false, prevMouseX = 0, prevMouseY = 0;
// Bezier Curve Trajectory (P0 -> P1 -> P2)
const P0 = new THREE.Vector3(-3.8, 1.0, 0); // Starting inside big box on left
const P1 = new THREE.Vector3(-0.6, 4.0, 0); // Parabolic arc apex
const P2 = new THREE.Vector3(1.6, 0.9, 0); // Cargo entry in truck
function initThree() {
scene = new THREE.Scene();
scene.background = new THREE.Color(themeBgHex[themeIndex]);
camera = new THREE.PerspectiveCamera(36, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(currentCamPos.x, currentCamPos.y, currentCamPos.z);
camera.lookAt(0, 0.3, 0);
renderer = new THREE.WebGLRenderer({
canvas: threeCanvas,
antialias: true,
preserveDrawingBuffer: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// Lighting Studio
const ambientLight = new THREE.AmbientLight(0xffffff, 0.85);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xfffaee, 1.25);
sunLight.position.set(8, 16, 9);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.near = 0.5;
sunLight.shadow.camera.far = 40;
sunLight.shadow.camera.left = -12;
sunLight.shadow.camera.right = 12;
sunLight.shadow.camera.top = 12;
sunLight.shadow.camera.bottom = -12;
sunLight.shadow.bias = -0.0005;
scene.add(sunLight);
const fillLight = new THREE.DirectionalLight(0xa7f3d0, 0.4);
fillLight.position.set(-10, 8, -4);
scene.add(fillLight);
// Ground Shadow Receiver
const groundGeo = new THREE.PlaneGeometry(80, 80);
const groundMat = new THREE.ShadowMaterial({ opacity: 0.15 });
groundPlane = new THREE.Mesh(groundGeo, groundMat);
groundPlane.rotation.x = -Math.PI / 2;
groundPlane.position.y = -1.45;
groundPlane.receiveShadow = true;
scene.add(groundPlane);
// Build 3D Models
build3DAddToCartButton();
buildFoldingCardboardBox();
buildParcelBox();
buildYellowDeliveryTruck();
buildTrajectoryArc();
buildExhaustParticleSystem();
// Event Listeners
window.addEventListener('resize', onResize);
window.addEventListener('mousemove', onMouseMove);
threeCanvas.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('mousemove', onMouseDrag);
threeCanvas.addEventListener('click', onCanvasClick);
// Touch Support
threeCanvas.addEventListener('touchstart', onTouchStart, { passive: true });
window.addEventListener('touchmove', onTouchMove, { passive: true });
window.addEventListener('touchend', onTouchEnd);
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
resizeConfetti();
}
// --- 2. Procedural Canvas Textures ---
function create3DButtonTexture() {
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 360;
const ctx = canvas.getContext('2d');
// Deep Teal Background
ctx.fillStyle = '#00766c';
ctx.fillRect(0, 0, 1024, 360);
// Inner subtle glow border
ctx.strokeStyle = '#00a396';
ctx.lineWidth = 14;
ctx.strokeRect(16, 16, 992, 328);
// Text "Add to Cart"
ctx.fillStyle = '#ffffff';
ctx.font = '900 84px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('Add to Cart', 140, 180);
// Shopping Cart Icon π on right
ctx.font = '90px sans-serif';
ctx.fillText('π', 740, 180);
return new THREE.CanvasTexture(canvas);
}
function createCardboardFrontTexture() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
// Kraft paper base
ctx.fillStyle = '#c89862';
ctx.fillRect(0, 0, 512, 512);
// Subtle paper noise/fibers
ctx.fillStyle = 'rgba(0, 0, 0, 0.03)';
for (let i = 0; i < 400; i++) {
ctx.fillRect(Math.random() * 512, Math.random() * 512, Math.random() * 4 + 1, 1);
}
// Packaging Icons (Dark brown ink #3d2314)
ctx.fillStyle = '#3d2314';
ctx.strokeStyle = '#3d2314';
ctx.lineWidth = 5;
// 1. Recycle Mobius Loop Icon (Left)
ctx.strokeRect(40, 320, 100, 100);
ctx.font = 'bold 52px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('β»', 90, 370);
// 2. Fragile Wine Glass Icon (Middle)
ctx.strokeRect(160, 320, 100, 100);
ctx.beginPath();
ctx.moveTo(190, 345);
ctx.lineTo(230, 345);
ctx.lineTo(230, 375);
ctx.quadraticCurveTo(230, 395, 210, 395);
ctx.quadraticCurveTo(190, 395, 190, 375);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(210, 395);
ctx.lineTo(210, 412);
ctx.moveTo(195, 412);
ctx.lineTo(225, 412);
ctx.stroke();
// Crack line
ctx.beginPath();
ctx.moveTo(205, 348);
ctx.lineTo(215, 362);
ctx.lineTo(208, 372);
ctx.stroke();
// 3. Up-Arrows Icon (Right)
ctx.strokeRect(280, 320, 100, 100);
ctx.font = 'bold 42px sans-serif';
ctx.fillText('ββ', 330, 370);
// Barcode at bottom right
ctx.fillRect(400, 330, 6, 80);
ctx.fillRect(412, 330, 3, 80);
ctx.fillRect(420, 330, 10, 80);
ctx.fillRect(435, 330, 4, 80);
ctx.fillRect(445, 330, 8, 80);
ctx.fillRect(458, 330, 5, 80);
ctx.fillRect(468, 330, 12, 80);
ctx.fillRect(485, 330, 4, 80);
// Kraft tape strip
ctx.fillStyle = '#b5824c';
ctx.fillRect(236, 0, 40, 512);
return new THREE.CanvasTexture(canvas);
}
function createShippingLabelTexture() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 384;
const ctx = canvas.getContext('2d');
// Kraft Parcel base
ctx.fillStyle = '#cf9d67';
ctx.fillRect(0, 0, 512, 384);
// Center packing tape
ctx.fillStyle = '#b88349';
ctx.fillRect(0, 168, 512, 48);
// White Shipping Label Sticker
ctx.fillStyle = '#ffffff';
ctx.fillRect(60, 40, 392, 304);
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 4;
ctx.strokeRect(60, 40, 392, 304);
// Header Stripe
ctx.fillStyle = '#0f172a';
ctx.fillRect(60, 40, 392, 48);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 24px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('PRIORITY 24H EXPRESS', 80, 74);
// Recipient Info
ctx.fillStyle = '#0f172a';
ctx.font = 'bold 22px sans-serif';
ctx.fillText('SHIP TO:', 80, 125);
ctx.font = '600 20px sans-serif';
ctx.fillText('Tanmoy - Logistics Fleet #24', 80, 155);
ctx.font = '16px sans-serif';
ctx.fillStyle = '#475569';
ctx.fillText('Express Dispatch Hub 07', 80, 182);
// Barcode Stripes
ctx.fillStyle = '#0f172a';
let bx = 80;
const widths = [6, 3, 10, 4, 8, 3, 12, 5, 7, 3, 9, 4, 11, 6, 8, 4, 10, 5, 8, 4, 12];
for (let w of widths) {
ctx.fillRect(bx, 215, w, 65);
bx += w + 6;
}
ctx.font = 'bold 18px monospace';
ctx.fillStyle = '#0f172a';
ctx.fillText('*TRK-8942-EXPRESS-2026*', 80, 310);
return new THREE.CanvasTexture(canvas);
}
function createTruckSideTexture() {
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 512;
const ctx = canvas.getContext('2d');
// Vibrant Yellow Livery
ctx.fillStyle = '#f5af19';
ctx.fillRect(0, 0, 1024, 512);
// Cargo panel bevel frame
ctx.strokeStyle = '#d99408';
ctx.lineWidth = 14;
ctx.strokeRect(30, 30, 964, 452);
// Subtle panel rib lines
ctx.strokeStyle = '#e29e0a';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(350, 30);
ctx.lineTo(350, 482);
ctx.moveTo(680, 30);
ctx.lineTo(680, 482);
ctx.stroke();
// Bold Express Logistics Typography
ctx.fillStyle = '#1e293b';
ctx.font = '900 64px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('EXPRESS LOGISTICS', 512, 240);
// Speed Accent Streak
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 28px sans-serif';
ctx.fillText('β‘ SAME-DAY AIR DISPATCH β‘', 512, 295);
return new THREE.CanvasTexture(canvas);
}
// --- 3. Build 3D Models ---
// A. 3D "Add to Cart π" Button in Center Stage (Exact match to Image 2)
function build3DAddToCartButton() {
if (btn3DGroup) scene.remove(btn3DGroup);
btn3DGroup = new THREE.Group();
const tealSideMat = new THREE.MeshStandardMaterial({
color: 0x005951,
roughness: 0.3,
metalness: 0.1
});
const tealFrontMat = new THREE.MeshStandardMaterial({
map: create3DButtonTexture(),
roughness: 0.25,
metalness: 0.1
});
const btnMats = [
tealSideMat, // Right
tealSideMat, // Left
tealSideMat, // Top
tealSideMat, // Bottom
tealFrontMat, // Front
tealSideMat // Back
];
const btnGeo = new THREE.BoxGeometry(4.6, 1.6, 0.45);
btn3DMesh = new THREE.Mesh(btnGeo, btnMats);
btn3DMesh.castShadow = true;
btn3DMesh.receiveShadow = true;
btn3DGroup.add(btn3DMesh);
btn3DGroup.position.set(0, 0.6, 0);
scene.add(btn3DGroup);
}
// B. Folding Cardboard Box (Exact match to Image 3 - media_1789995029407.png)
function buildFoldingCardboardBox() {
if (outerBoxGroup) scene.remove(outerBoxGroup);
outerBoxGroup = new THREE.Group();
boxFlaps = {};
const kraftMat = new THREE.MeshStandardMaterial({
color: 0xc99b67,
roughness: 0.85,
metalness: 0.05
});
const frontMat = new THREE.MeshStandardMaterial({
map: createCardboardFrontTexture(),
roughness: 0.85
});
const boxMaterials = [
kraftMat, // Right
kraftMat, // Left
kraftMat, // Top (open)
kraftMat, // Bottom
frontMat, // Front
kraftMat // Back
];
// Main Box Base Container (Width 2.5, Height 1.6, Depth 2.4)
const baseGeo = new THREE.BoxGeometry(2.5, 1.6, 2.4);
const baseMesh = new THREE.Mesh(baseGeo, boxMaterials);
baseMesh.position.set(0, 0.8, 0);
baseMesh.castShadow = true;
baseMesh.receiveShadow = true;
outerBoxGroup.add(baseMesh);
// 4 Animated Hinged Flaps (Opening Outward as in Image 3)
const flapThickness = 0.04;
// 1. Front Flap (Hinged at top-front z = 1.2, y = 1.6)
const flapFGroup = new THREE.Group();
flapFGroup.position.set(0, 1.6, 1.2);
const flapF = new THREE.Mesh(new THREE.BoxGeometry(2.48, 0.95, flapThickness), kraftMat);
flapF.position.set(0, 0.475, 0);
flapF.castShadow = true;
flapFGroup.add(flapF);
outerBoxGroup.add(flapFGroup);
boxFlaps.front = flapFGroup;
// 2. Back Flap (Hinged at top-back z = -1.2, y = 1.6)
const flapBGroup = new THREE.Group();
flapBGroup.position.set(0, 1.6, -1.2);
const flapB = new THREE.Mesh(new THREE.BoxGeometry(2.48, 0.95, flapThickness), kraftMat);
flapB.position.set(0, 0.475, 0);
flapB.castShadow = true;
flapBGroup.add(flapB);
outerBoxGroup.add(flapBGroup);
boxFlaps.back = flapBGroup;
// 3. Left Flap (Hinged at top-left x = -1.25, y = 1.6)
const flapLGroup = new THREE.Group();
flapLGroup.position.set(-1.25, 1.6, 0);
const flapL = new THREE.Mesh(new THREE.BoxGeometry(flapThickness, 0.95, 2.38), kraftMat);
flapL.position.set(0, 0.475, 0);
flapL.castShadow = true;
flapLGroup.add(flapL);
outerBoxGroup.add(flapLGroup);
boxFlaps.left = flapLGroup;
// 4. Right Flap (Hinged at top-right x = 1.25, y = 1.6)
const flapRGroup = new THREE.Group();
flapRGroup.position.set(1.25, 1.6, 0);
const flapR = new THREE.Mesh(new THREE.BoxGeometry(flapThickness, 0.95, 2.38), kraftMat);
flapR.position.set(0, 0.475, 0);
flapR.castShadow = true;
flapRGroup.add(flapR);
outerBoxGroup.add(flapRGroup);
boxFlaps.right = flapRGroup;
// Initial State: Hidden until 3D button morphs
outerBoxGroup.position.set(0, -1.45, 0);
outerBoxGroup.scale.set(0.001, 0.001, 0.001);
outerBoxGroup.visible = false;
scene.add(outerBoxGroup);
}
// C. Parcel Box (The package inside that rises & leaps)
function buildParcelBox() {
if (parcelGroup) scene.remove(parcelGroup);
parcelGroup = new THREE.Group();
const parcelMat = new THREE.MeshStandardMaterial({
color: 0xcf9d67,
roughness: 0.85
});
const labelMat = new THREE.MeshStandardMaterial({
map: createShippingLabelTexture(),
roughness: 0.7
});
const parcelMaterials = [
parcelMat, // Right
parcelMat, // Left
labelMat, // Top (Shipping label)
parcelMat, // Bottom
parcelMat, // Front
parcelMat // Back
];
const pGeo = new THREE.BoxGeometry(1.4, 1.05, 1.3);
const pMesh = new THREE.Mesh(pGeo, parcelMaterials);
pMesh.castShadow = true;
parcelGroup.add(pMesh);
// Initial state: hidden inside box
parcelGroup.position.set(0, -0.6, 0);
parcelGroup.scale.set(0.001, 0.001, 0.001);
parcelGroup.visible = false;
scene.add(parcelGroup);
}
// D. Yellow Delivery Truck on Right (x = 3.2, cab on right facing +X)
function buildYellowDeliveryTruck() {
if (truckGroup) scene.remove(truckGroup);
truckGroup = new THREE.Group();
truckBodyGroup = new THREE.Group();
wheels = [];
const yellowPaint = new THREE.MeshStandardMaterial({
color: 0xf5af19,
roughness: 0.35,
metalness: 0.15
});
const cargoSideMat = new THREE.MeshStandardMaterial({
map: createTruckSideTexture(),
roughness: 0.35
});
const chassisMat = new THREE.MeshStandardMaterial({ color: 0x1f2937, roughness: 0.85 });
const chromeMat = new THREE.MeshStandardMaterial({ color: 0xe2e8f0, metalness: 0.9, roughness: 0.15 });
const glassMat = new THREE.MeshStandardMaterial({
color: 0x67e8f9,
roughness: 0.1,
metalness: 0.8,
transparent: true,
opacity: 0.75
});
const lightMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xfffaed,
emissiveIntensity: 0.8
});
const redLightMat = new THREE.MeshStandardMaterial({
color: 0xef4444,
emissive: 0xdc2626,
emissiveIntensity: 0.6
});
// 1. Cargo Container Box (Length 3.8, Height 2.4, Depth 2.1)
const cargoMaterials = [
yellowPaint, // Right (+X towards cab)
chassisMat, // Left (-X rear open cargo interior)
yellowPaint, // Top
chassisMat, // Bottom
cargoSideMat, // Front (+Z facing viewer)
cargoSideMat // Back (-Z)
];
const cargoMesh = new THREE.Mesh(new THREE.BoxGeometry(3.8, 2.4, 2.1), cargoMaterials);
cargoMesh.position.set(0, 0.7, 0);
cargoMesh.castShadow = true;
cargoMesh.receiveShadow = true;
truckBodyGroup.add(cargoMesh);
// Roof & Corner Trim Bevels
const roofTrim = new THREE.Mesh(new THREE.BoxGeometry(3.9, 0.1, 2.2), yellowPaint);
roofTrim.position.set(0, 1.95, 0);
roofTrim.castShadow = true;
truckBodyGroup.add(roofTrim);
// 2. Open Rear Cargo Doors on Left end of Truck (x = -1.9)
const doorGeo = new THREE.BoxGeometry(0.06, 2.3, 1.02);
const doorMat = new THREE.MeshStandardMaterial({ color: 0xf5af19, roughness: 0.35 });
// Left Door (Hinged at x = -1.9, z = 1.05)
truckDoorLeft = new THREE.Group();
truckDoorLeft.position.set(-1.9, 0.7, 1.05);
const dMeshL = new THREE.Mesh(doorGeo, doorMat);
dMeshL.position.set(0, 0, -0.51);
dMeshL.castShadow = true;
truckDoorLeft.add(dMeshL);
// Add chrome lock rod & red reflector
const rodL = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 2.2, 8), chromeMat);
rodL.position.set(-0.04, 0, -0.51);
truckDoorLeft.add(rodL);
const refL = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.15, 0.3), redLightMat);
refL.position.set(-0.04, -0.8, -0.51);
truckDoorLeft.add(refL);
// Right Door (Hinged at x = -1.9, z = -1.05)
truckDoorRight = new THREE.Group();
truckDoorRight.position.set(-1.9, 0.7, -1.05);
const dMeshR = new THREE.Mesh(doorGeo, doorMat);
dMeshR.position.set(0, 0, 0.51);
dMeshR.castShadow = true;
truckDoorRight.add(dMeshR);
const rodR = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 2.2, 8), chromeMat);
rodR.position.set(-0.04, 0, 0.51);
truckDoorRight.add(rodR);
const refR = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.15, 0.3), redLightMat);
refR.position.set(-0.04, -0.8, 0.51);
truckDoorRight.add(refR);
// Initially wide OPEN facing the cardboard box (matching Image 4!)
truckDoorLeft.rotation.y = -Math.PI * 0.55;
truckDoorRight.rotation.y = Math.PI * 0.55;
truckBodyGroup.add(truckDoorLeft);
truckBodyGroup.add(truckDoorRight);
// 3. Driver Cab on Right (+X facing right)
const cabGroup = new THREE.Group();
cabGroup.position.set(2.4, 0.45, 0);
// Main Cab Lower Body
const cabLower = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1.5, 1.95), yellowPaint);
cabLower.position.set(0, 0, 0);
cabLower.castShadow = true;
cabGroup.add(cabLower);
// Cab Upper Windshield Angled Section
const cabUpper = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.0, 1.85), yellowPaint);
cabUpper.position.set(-0.15, 1.05, 0);
cabUpper.castShadow = true;
cabGroup.add(cabUpper);
// Front Windshield Glass
const windshield = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.85, 1.7), glassMat);
windshield.position.set(0.48, 1.05, 0);
windshield.rotation.z = -0.15;
cabGroup.add(windshield);
// Side Driver Windows
[-0.94, 0.94].forEach(zPos => {
const sideWin = new THREE.Mesh(new THREE.BoxGeometry(0.85, 0.7, 0.05), glassMat);
sideWin.position.set(-0.1, 1.05, zPos);
cabGroup.add(sideWin);
// Side Mirrors
const mirror = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.32, 0.08), chassisMat);
mirror.position.set(0.35, 0.85, zPos > 0 ? zPos + 0.18 : zPos - 0.18);
cabGroup.add(mirror);
});
// Front Grille & Bumper
const grille = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.55, 1.6), chassisMat);
grille.position.set(0.82, -0.1, 0);
cabGroup.add(grille);
const bumper = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.35, 2.05), chassisMat);
bumper.position.set(0.82, -0.55, 0);
bumper.castShadow = true;
cabGroup.add(bumper);
// Front Headlights
[-0.65, 0.65].forEach(zPos => {
const lamp = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.16, 0.08, 16), lightMat);
lamp.rotation.z = Math.PI / 2;
lamp.position.set(0.83, 0.05, zPos);
cabGroup.add(lamp);
});
truckBodyGroup.add(cabGroup);
// 4. Chassis Frame & Underbody
const chassis = new THREE.Mesh(new THREE.BoxGeometry(5.2, 0.3, 1.8), chassisMat);
chassis.position.set(0.6, -0.45, 0);
chassis.castShadow = true;
truckBodyGroup.add(chassis);
// Add truckBodyGroup to truckGroup
truckGroup.add(truckBodyGroup);
// 5. Wheels (2 Front, 2 Rear)
const wheelGeo = new THREE.CylinderGeometry(0.62, 0.62, 0.44, 24);
const tireMat = new THREE.MeshStandardMaterial({ color: 0x111827, roughness: 0.95 });
const hubMat = new THREE.MeshStandardMaterial({ color: 0xe2e8f0, metalness: 0.85, roughness: 0.2 });
const hubGeo = new THREE.CylinderGeometry(0.34, 0.34, 0.46, 16);
const wheelPos = [
{ x: -0.9, z: 1.15 },
{ x: -0.9, z: -1.15 },
{ x: 2.5, z: 1.15 },
{ x: 2.5, z: -1.15 }
];
wheelPos.forEach(p => {
const wGroup = new THREE.Group();
wGroup.position.set(p.x, -0.85, p.z);
const tire = new THREE.Mesh(wheelGeo, tireMat);
tire.rotation.x = Math.PI / 2;
tire.castShadow = true;
const hub = new THREE.Mesh(hubGeo, hubMat);
hub.rotation.x = Math.PI / 2;
wGroup.add(tire);
wGroup.add(hub);
truckGroup.add(wGroup);
wheels.push(wGroup);
});
// Parked in Scene on Right Floor (x = 3.2), initially invisible/off-frame until wide view
truckGroup.position.set(3.2, -0.55, 0);
truckGroup.visible = false;
scene.add(truckGroup);
}
// E. 3D Trajectory Curve & Arrow (P0 -> P1 -> P2)
function buildTrajectoryArc() {
if (trajectoryGroup) scene.remove(trajectoryGroup);
trajectoryGroup = new THREE.Group();
const curve = new THREE.QuadraticBezierCurve3(P0, P1, P2);
const points = curve.getPoints(36);
// Dotted beads along curve
const beadGeo = new THREE.SphereGeometry(0.045, 8, 8);
const beadMat = new THREE.MeshStandardMaterial({
color: 0x1e293b,
roughness: 0.5
});
points.forEach((pt, idx) => {
if (idx % 2 === 0 && idx < points.length - 2) {
const bead = new THREE.Mesh(beadGeo, beadMat);
bead.position.copy(pt);
trajectoryGroup.add(bead);
}
});
// Arrow Cone at P2 pointing down-right into truck cargo door
const arrowGeo = new THREE.ConeGeometry(0.18, 0.45, 12);
const arrowMat = new THREE.MeshStandardMaterial({ color: 0x1e293b });
const arrow = new THREE.Mesh(arrowGeo, arrowMat);
arrow.position.set(P2.x, P2.y, P2.z);
arrow.rotation.z = -Math.PI * 0.4;
trajectoryGroup.add(arrow);
trajectoryGroup.visible = false;
scene.add(trajectoryGroup);
}
// F. Exhaust Particle System
function buildExhaustParticleSystem() {
const puffGeo = new THREE.SphereGeometry(0.15, 8, 8);
const puffMat = new THREE.MeshBasicMaterial({
color: 0xf1f5f9,
transparent: true,
opacity: 0
});
for (let i = 0; i < 16; i++) {
const p = new THREE.Mesh(puffGeo, puffMat.clone());
p.visible = false;
scene.add(p);
exhaustParticles.push({
mesh: p,
life: 0,
vx: 0,
vy: 0,
vz: 0
});
}
}
function emitExhaustPuff(originX, originY, originZ) {
const p = exhaustParticles.find(pt => pt.life <= 0);
if (!p) return;
p.mesh.position.set(originX, originY, originZ);
p.mesh.scale.set(0.6, 0.6, 0.6);
p.mesh.material.opacity = 0.75;
p.mesh.visible = true;
p.life = 1.0;
p.vx = -(0.06 + Math.random() * 0.05);
p.vy = 0.02 + Math.random() * 0.03;
p.vz = (Math.random() - 0.5) * 0.04;
}
function updateExhaustParticles() {
exhaustParticles.forEach(p => {
if (p.life > 0) {
p.life -= 0.035 * animSpeed;
p.mesh.position.x += p.vx;
p.mesh.position.y += p.vy;
p.mesh.position.z += p.vz;
p.mesh.scale.addScalar(0.04);
p.mesh.material.opacity = p.life * 0.75;
if (p.life <= 0) {
p.mesh.visible = false;
}
}
});
}
// --- 4. Web Audio Synthesizer Engine ---
function initAudio() {
if (!audioCtx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playTone(freq, dur, type = 'sine', vol = 0.25) {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.001, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(vol, audioCtx.currentTime + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + dur);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + dur);
}
function playWhooshSound() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(180, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(640, audioCtx.currentTime + 0.6);
osc.frequency.exponentialRampToValueAtTime(320, audioCtx.currentTime + 1.4);
gain.gain.setValueAtTime(0.01, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + 0.5);
gain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + 1.4);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 1.4);
}
function playImpactSound() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(140, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(35, audioCtx.currentTime + 0.35);
gain.gain.setValueAtTime(0.35, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.35);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.35);
}
function playLatchSound() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
[520, 780].forEach((f, i) => {
setTimeout(() => {
playTone(f, 0.12, 'square', 0.15);
}, i * 70);
});
}
function playEngineSound() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(70, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(240, audioCtx.currentTime + 1.5);
osc.frequency.exponentialRampToValueAtTime(110, audioCtx.currentTime + 2.8);
gain.gain.setValueAtTime(0.09, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + 2.8);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 2.8);
}
// --- 5. User Interaction (Canvas Click on 3D Button) ---
function onMouseMove(e) {
const rect = threeCanvas.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
// Hover effect on 3D button
if (!isAnimating && btn3DGroup.visible) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([btn3DMesh]);
if (intersects.length > 0) {
threeCanvas.style.cursor = 'pointer';
btn3DGroup.scale.set(1.04, 1.04, 1.04);
} else {
threeCanvas.style.cursor = isDragging ? 'grabbing' : 'grab';
btn3DGroup.scale.set(1.0, 1.0, 1.0);
}
}
if (isDragging) return;
targetRotY = mouse.x * 0.14;
targetRotX = -mouse.y * 0.09;
}
function onMouseDown(e) {
isDragging = true;
prevMouseX = e.clientX;
prevMouseY = e.clientY;
}
function onMouseUp() {
isDragging = false;
}
function onMouseDrag(e) {
if (!isDragging) return;
const deltaX = e.clientX - prevMouseX;
const deltaY = e.clientY - prevMouseY;
targetRotY += deltaX * 0.005;
targetRotX += deltaY * 0.005;
targetRotX = Math.max(-0.25, Math.min(0.35, targetRotX));
targetRotY = Math.max(-0.6, Math.min(0.6, targetRotY));
prevMouseX = e.clientX;
prevMouseY = e.clientY;
}
let touchStartX = 0, touchStartY = 0;
function onTouchStart(e) {
if (e.touches.length === 1) {
isDragging = true;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}
function onTouchMove(e) {
if (!isDragging || e.touches.length !== 1) return;
const deltaX = e.touches[0].clientX - touchStartX;
const deltaY = e.touches[0].clientY - touchStartY;
targetRotY += deltaX * 0.006;
targetRotX += deltaY * 0.006;
targetRotX = Math.max(-0.25, Math.min(0.35, targetRotX));
targetRotY = Math.max(-0.6, Math.min(0.6, targetRotY));
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
function onTouchEnd() {
isDragging = false;
}
function onCanvasClick(e) {
if (isAnimating || !btn3DGroup.visible) return;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([btn3DMesh]);
if (intersects.length > 0) {
startDeliverySequence();
}
}
// --- 6. Step-by-Step Delivery Choreography ---
btnAddCart.addEventListener('click', startDeliverySequence);
btnReplayDelivery.addEventListener('click', resetAndReplay);
btnResetView.addEventListener('click', resetToProductView);
function setStep(num, statusText) {
if (statusText) stepperStatusText.innerText = statusText;
for (let i = 1; i <= 4; i++) {
const stepEl = document.getElementById(`step${i}`);
const lineEl = document.getElementById(`line${i}`);
if (stepEl) {
if (i <= num) stepEl.classList.add('active');
else stepEl.classList.remove('active');
}
if (lineEl) {
if (i < num) lineEl.classList.add('active');
else lineEl.classList.remove('active');
}
}
}
function startDeliverySequence() {
if (isAnimating) return;
isAnimating = true;
// UI Stage transition
cartActionStage.classList.add('hide-stage');
statusStepper.classList.remove('hidden');
receiptModal.classList.add('hidden');
// Button click tone
playTone(523.25, 0.35, 'triangle', 0.3); // C5
// Step 1: 3D Button Morphs & Cardboard Box Unfolds (Image 3 - media_1789995029407.png)
setStep(1, '3D Button Morphing & Unfolding Cardboard Box...');
playTone(392, 0.3, 'sine', 0.2);
outerBoxGroup.visible = true;
parcelGroup.visible = true;
outerBoxGroup.position.set(0, -1.45, 0);
parcelGroup.position.set(0, -0.6, 0);
let t = 0;
const step1Timer = setInterval(() => {
t += 0.025 * animSpeed;
const p = Math.min(1, t);
// Button shrinks & squashes
btn3DGroup.scale.set(Math.max(0.001, 1 - p * 1.1), Math.max(0.001, 1 - p * 1.1), Math.max(0.001, 1 - p * 1.1));
if (p > 0.8) btn3DGroup.visible = false;
// Box scales up into center
const boxScale = Math.min(1, p * 1.2);
outerBoxGroup.scale.set(boxScale, boxScale, boxScale);
// Flaps fold open outward (~35-45 deg like Image 3)
const flapAngle = (Math.PI * 0.32) * p;
boxFlaps.front.rotation.x = flapAngle;
boxFlaps.back.rotation.x = -flapAngle;
boxFlaps.left.rotation.z = flapAngle;
boxFlaps.right.rotation.z = -flapAngle;
// Parcel rises out of box
parcelGroup.scale.set(boxScale, boxScale, boxScale);
parcelGroup.position.y = -0.6 + p * 1.5; // reaches y = 0.9
if (p >= 1) {
clearInterval(step1Timer);
// Pause so user sees the unfolded box and parcel
setTimeout(phase2_WidePanAndTruckReveal, 500 / animSpeed);
}
}, 25);
}
function phase2_WidePanAndTruckReveal() {
// Camera glides to wide view, box translates to left x = -3.8, truck appears on right x = 3.2
targetCamPos = { ...camPosWide };
truckGroup.visible = true;
trajectoryGroup.visible = true;
let t = 0;
const panTimer = setInterval(() => {
t += 0.025 * animSpeed;
const p = Math.min(1, t);
const easeP = 1 - Math.pow(1 - p, 3);
// Slide box to left (0 -> -3.8)
outerBoxGroup.position.x = -3.8 * easeP;
parcelGroup.position.x = -3.8 * easeP;
// Flaps adjust: front/right flap fold down, left flap stays open lid
boxFlaps.front.rotation.x = Math.PI * 0.32 * (1 - easeP);
boxFlaps.back.rotation.x = -Math.PI * 0.32 * (1 - easeP);
boxFlaps.right.rotation.z = -Math.PI * 0.32 * (1 - easeP);
boxFlaps.left.rotation.z = Math.PI * 0.35; // Top left open lid matching Image 4!
if (p >= 1) {
clearInterval(panTimer);
// Pause before parcel leaps into truck
setTimeout(phase3_ParabolicJump, 400 / animSpeed);
}
}, 25);
}
function phase3_ParabolicJump() {
// Phase 2 Stepper: Trajectory Flight into Truck Cargo Bay (Image 4 - media_1789995029409.png)
setStep(2, 'Loading Parcel Along Trajectory Arc into Delivery Van...');
playWhooshSound();
let t = 0;
const curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(-3.8, 0.9, 0),
P1,
P2
);
const step3Timer = setInterval(() => {
t += 0.015 * animSpeed;
const p = Math.min(1, t);
// Quadratic Bezier Position
const pos = curve.getPoint(p);
parcelGroup.position.copy(pos);
// 3D Flight Rotation
parcelGroup.rotation.z = Math.sin(p * Math.PI) * 0.6;
parcelGroup.rotation.y = p * Math.PI * 1.5;
if (p >= 1) {
clearInterval(step3Timer);
// Parcel lands inside cargo bay
parcelGroup.visible = false;
playImpactSound();
// Truck Suspension Bounce (squash & recoil physics)
let st = 0;
const springTimer = setInterval(() => {
st += 0.12 * animSpeed;
truckBodyGroup.position.y = -Math.sin(st * Math.PI * 2) * Math.exp(-st * 1.5) * 0.18;
if (st >= 2.0) {
clearInterval(springTimer);
truckBodyGroup.position.y = 0;
}
}, 20);
// Pause so user sees successful cargo entry
setTimeout(phase4_CloseDoors, 500 / animSpeed);
}
}, 25);
}
function phase4_CloseDoors() {
// Phase 3 Stepper: Cargo Doors Swing Shut & Latch
setStep(3, 'Securing Cargo Doors & Sealing Package Bay...');
playTone(440, 0.3, 'sine', 0.2);
let t = 0;
const step4Timer = setInterval(() => {
t += 0.025 * animSpeed;
const p = Math.min(1, t);
// Doors swing from open (+/- 0.55 * PI) to closed (0)
truckDoorLeft.rotation.y = -Math.PI * 0.55 * (1 - p);
truckDoorRight.rotation.y = Math.PI * 0.55 * (1 - p);
// Fade out trajectory arc
if (trajectoryGroup) {
trajectoryGroup.position.y = -p * 2.0;
}
if (p >= 1) {
clearInterval(step4Timer);
playLatchSound();
// Pause before truck accelerates
setTimeout(phase5_DepartAndAccelerate, 600 / animSpeed);
}
}, 25);
}
function phase5_DepartAndAccelerate() {
// Phase 4 Stepper: Engine Rev & Out for Delivery (Image 1 / 5 - media_1789995029405.png)
setStep(4, 'Express Delivery Van Dispatched! Speeding to Destination...');
playEngineSound();
let t = 0;
let truckSpeed = 0;
const step5Timer = setInterval(() => {
t += 0.016 * animSpeed;
const p = Math.min(1, t);
// Acceleration physics
truckSpeed += 0.008 * animSpeed;
truckGroup.position.x += truckSpeed * 2.8;
// Spin wheels
wheels.forEach(w => {
w.rotation.z -= truckSpeed * 1.8;
});
// Emit exhaust smoke puffs behind truck
if (Math.random() < 0.45) {
emitExhaustPuff(truckGroup.position.x - 1.8, -0.9, 0);
}
if (truckGroup.position.x > 26 || p >= 1) {
clearInterval(step5Timer);
// Phase 6: Confetti & Receipt Modal
setTimeout(phase6_Celebration, 400 / animSpeed);
}
}, 25);
}
function phase6_Celebration() {
fireConfetti();
playTone(659.25, 0.4, 'triangle', 0.3); // E5
setTimeout(() => playTone(783.99, 0.5, 'triangle', 0.35), 180); // G5
receiptItemName.innerText = products[currentProductIdx].name;
receiptModal.classList.remove('hidden');
statusStepper.classList.add('hidden');
isAnimating = false;
}
function resetSceneState() {
// Reset camera position to close focus
targetCamPos = { ...camPosClose };
currentCamPos = { ...camPosClose };
// Reset 3D Button
btn3DGroup.visible = true;
btn3DGroup.scale.set(1, 1, 1);
btn3DGroup.position.set(0, 0.6, 0);
// Reset Cardboard Box
outerBoxGroup.visible = false;
outerBoxGroup.scale.set(0.001, 0.001, 0.001);
outerBoxGroup.position.set(0, -1.45, 0);
boxFlaps.front.rotation.x = 0;
boxFlaps.back.rotation.x = 0;
boxFlaps.left.rotation.z = 0;
boxFlaps.right.rotation.z = 0;
// Reset Parcel
parcelGroup.visible = false;
parcelGroup.scale.set(0.001, 0.001, 0.001);
parcelGroup.position.set(0, -0.6, 0);
parcelGroup.rotation.set(0, 0, 0);
// Reset Truck
truckGroup.visible = false;
truckGroup.position.set(3.2, -0.55, 0);
truckBodyGroup.position.y = 0;
truckDoorLeft.rotation.y = -Math.PI * 0.55;
truckDoorRight.rotation.y = Math.PI * 0.55;
// Reset Trajectory
if (trajectoryGroup) {
trajectoryGroup.visible = false;
trajectoryGroup.position.y = 0;
}
}
function resetAndReplay() {
receiptModal.classList.add('hidden');
resetSceneState();
setTimeout(startDeliverySequence, 300);
}
function resetToProductView() {
receiptModal.classList.add('hidden');
statusStepper.classList.add('hidden');
resetSceneState();
cartActionStage.classList.remove('hide-stage');
isAnimating = false;
}
// --- 7. Speed Controller & Product Switcher ---
speedBtns.forEach(btn => {
btn.addEventListener('click', () => {
speedBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
animSpeed = parseFloat(btn.dataset.speed) || 1.0;
});
});
// Audio Toggle
btnAudioToggle.addEventListener('click', () => {
audioEnabled = !audioEnabled;
btnAudioToggle.innerHTML = audioEnabled ? '<i class="fas fa-volume-up"></i>' : '<i class="fas fa-volume-xmark"></i>';
btnAudioToggle.style.color = audioEnabled ? 'var(--text-main)' : '#94a3b8';
});
// Theme Toggle
btnThemeToggle.addEventListener('click', () => {
themeIndex = (themeIndex + 1) % themeClasses.length;
app.className = 'cart-truck-app ' + themeClasses[themeIndex];
if (scene) {
scene.background.setHex(themeBgHex[themeIndex]);
}
});
// Product Selection Modal
btnProductModal.addEventListener('click', () => {
productModal.classList.remove('hidden');
});
btnCloseModal.addEventListener('click', () => {
productModal.classList.add('hidden');
});
const optCards = document.querySelectorAll('.product-option-card');
optCards.forEach(card => {
card.addEventListener('click', () => {
optCards.forEach(c => c.classList.remove('active'));
card.classList.add('active');
currentProductIdx = parseInt(card.dataset.product) || 0;
updateProductDetails();
});
});
btnSelectProductDone.addEventListener('click', () => {
productModal.classList.add('hidden');
});
function updateProductDetails() {
const prod = products[currentProductIdx];
dispProductCat.innerHTML = `<i class="fas ${prod.icon} me-1"></i> ${prod.cat}`;
dispProductName.innerText = prod.name;
dispProductPrice.innerText = prod.price;
dispProductDesc.innerText = prod.desc;
}
// --- 8. Confetti Celebration Cannon ---
let confettiCtx = confettiCanvas.getContext('2d');
let confettiParticles = [];
let confettiAnimFrame = null;
function resizeConfetti() {
confettiCanvas.width = window.innerWidth;
confettiCanvas.height = window.innerHeight;
}
resizeConfetti();
function fireConfetti() {
confettiParticles = [];
const colors = ['#f59e0b', '#10b981', '#3b82f6', '#ec4899', '#8b5cf6', '#ffffff'];
for (let i = 0; i < 90; i++) {
confettiParticles.push({
x: window.innerWidth / 2,
y: window.innerHeight * 0.45,
vx: (Math.random() - 0.5) * 16,
vy: -Math.random() * 14 - 4,
size: Math.random() * 8 + 6,
color: colors[Math.floor(Math.random() * colors.length)],
rotation: Math.random() * Math.PI * 2,
vRot: (Math.random() - 0.5) * 0.2,
life: 1.0
});
}
if (!confettiAnimFrame) {
animateConfetti();
}
}
function animateConfetti() {
confettiCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);
let activeCount = 0;
confettiParticles.forEach(p => {
if (p.life > 0) {
activeCount++;
p.x += p.vx;
p.y += p.vy;
p.vy += 0.35; // Gravity
p.rotation += p.vRot;
p.life -= 0.012;
confettiCtx.save();
confettiCtx.translate(p.x, p.y);
confettiCtx.rotate(p.rotation);
confettiCtx.fillStyle = p.color;
confettiCtx.globalAlpha = Math.max(0, p.life);
confettiCtx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6);
confettiCtx.restore();
}
});
if (activeCount > 0) {
confettiAnimFrame = requestAnimationFrame(animateConfetti);
} else {
confettiCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);
confettiAnimFrame = null;
}
}
// --- 9. Render Loop ---
let clockTime = 0;
function animate() {
requestAnimationFrame(animate);
clockTime += 0.02;
// Subtle breathing hover on 3D Button in initial idle state
if (!isAnimating && btn3DGroup.visible) {
btn3DGroup.position.y = 0.6 + Math.sin(clockTime * 2) * 0.06;
}
// Camera Easing (transition between close focus and wide focus)
currentCamPos.x += (targetCamPos.x - currentCamPos.x) * 0.06;
currentCamPos.y += (targetCamPos.y - currentCamPos.y) * 0.06;
currentCamPos.z += (targetCamPos.z - currentCamPos.z) * 0.06;
// Smooth Parallax Camera Orbit
currentRotX += (targetRotX - currentRotX) * 0.08;
currentRotY += (targetRotY - currentRotY) * 0.08;
camera.position.x = currentCamPos.x + Math.sin(currentRotY) * currentCamPos.z;
camera.position.z = Math.cos(currentRotY) * currentCamPos.z;
camera.position.y = currentCamPos.y + currentRotX * 4.5;
camera.lookAt(0, 0.3, 0);
// Update Particles
updateExhaustParticles();
renderer.render(scene, camera);
}
// Boot
initThree();
animate();
})();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.
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
add-to-cart-3d-truck-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.