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

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

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

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

live-sandbox://add-to-cart-3d-truck

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.

HTML (index.html)
<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 &lsquo;Add to Cart&rsquo; 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 &bull; 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>&ldquo;Add to Cart πŸ›’&rdquo;</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 &bull; 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">&times;</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 &bull; 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 &bull; 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 &bull; 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 &bull; 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.

CSS (style.css)
/* 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:

JavaScript (script.js)
(() => {
  // 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

Three.js 3D WebGL

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

Interactive 3D Button Morphing

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

Flap Physics & Box Unfolding

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

Parabolic Quadratic Bezier Curves

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

Suspension Physics & Spring Damping

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

Keyframe Delivery Choreography

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

Web Audio Synthesizer

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

4. How to Run Locally on Your Computer

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

Bonus Challenges for Learners

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

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

Other Beginner Projects You Might Like

Advanced45 mins
Creative & 3D WebGL

3D Interactive Birthday Heart Tree Surprise

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

Three.js WebGLProcedural 3D TreesPhysics Slingshot+3
Advanced35 mins
Creative & 3D WebGL

3D Radial Share Arc Button Animation

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

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

3D Cosmic Memory Carousel & Flip Card Animation

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

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