Interactive 3D Panda Web Project with Three.js

A comprehensive frontend demonstration utilizing Three.js, JavaScript, and Bootstrap 5 to render responsive 3D graphics and interactive animations on the web.

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

Interactive 3D Panda Web Project with Three.js

live-sandbox://interactive-3d-panda

1. Project Overview & Features

The Interactive 3D Panda Web Project with Three.js is an essential frontend project designed to help you bridge the gap between learning JavaScript syntax and building interactive, responsive web applications.

Key Features Implemented:

  • Procedural 3D Panda character with head, ears, eyes, paws, body, and bamboo stalk
  • Smooth mouse and touch tracking: Panda head and eyes follow the cursor across the screen
  • Bootstrap 5 Action Dock: Feed Bamboo πŸŽ‹, Wave Hello πŸ‘‹, Happy Dance πŸ•Ί, Sleep/Nap 😴, and Cool Shades πŸ•ΆοΈ
  • Lighting Studio: β˜€οΈ Daylight Studio, 🌸 Sunset Glow, and πŸŒ™ Cyber Neon Night
  • Interactive 360Β° drag-to-orbit rotation and automatic responsive canvas resizing
  • Particle Emitters: Floating celebration hearts, bamboo leaves, and Zzz sleep bubbles

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="panda-3d-app">
  <!-- Top Navigation & Mood Studio Bar -->
  <div class="app-top-bar d-flex justify-content-between align-items-center p-3">
    <div class="d-flex align-items-center gap-2">
      <span class="badge bg-emerald text-white px-3 py-2 rounded-pill font-monospace"><i class="fas fa-cube me-1"></i> Three.js WebGL</span>
      <h5 class="m-0 text-white fw-bold d-none d-sm-inline">Interactive 3D Panda</h5>
    </div>
    
    <!-- Lighting Studio & View Controls -->
    <div class="d-flex align-items-center gap-2">
      <div class="btn-group btn-group-sm" role="group">
        <button type="button" class="btn btn-outline-light active" id="theme-day" title="Daylight Studio">β˜€οΈ Day</button>
        <button type="button" class="btn btn-outline-light" id="theme-sunset" title="Sunset Glow">🌸 Sunset</button>
        <button type="button" class="btn btn-outline-light" id="theme-night" title="Cyber Neon Night">πŸŒ™ Neon</button>
      </div>
      <button type="button" class="btn btn-sm btn-outline-info" id="btn-reset-cam" title="Reset Camera View"><i class="fas fa-camera"></i> Reset</button>
    </div>
  </div>

  <!-- 3D WebGL Canvas Viewport -->
  <div class="viewport-wrapper position-relative" id="panda-canvas-container">
    <canvas id="panda-canvas"></canvas>

    <!-- Floating Interactive Action Pill Menu (Bootstrap 5) -->
    <div class="action-dock-container">
      <div class="action-dock d-flex flex-wrap justify-content-center gap-2 p-2">
        <button type="button" class="btn btn-dock active" id="act-feed" data-action="feed">
          <span class="emoji">πŸŽ‹</span> <span class="d-none d-md-inline">Feed Bamboo</span>
        </button>
        <button type="button" class="btn btn-dock" id="act-wave" data-action="wave">
          <span class="emoji">πŸ‘‹</span> <span class="d-none d-md-inline">Wave Hello</span>
        </button>
        <button type="button" class="btn btn-dock" id="act-dance" data-action="dance">
          <span class="emoji">πŸ•Ί</span> <span class="d-none d-md-inline">Happy Dance</span>
        </button>
        <button type="button" class="btn btn-dock" id="act-sleep" data-action="sleep">
          <span class="emoji">😴</span> <span class="d-none d-md-inline">Sleep / Nap</span>
        </button>
        <button type="button" class="btn btn-dock" id="act-glasses" data-action="glasses">
          <span class="emoji">πŸ•ΆοΈ</span> <span class="d-none d-md-inline">Cool Shades</span>
        </button>
      </div>
    </div>

    <!-- Live Status Overlay -->
    <div class="status-overlay text-center">
      <div class="status-bubble" id="panda-status">
        <span class="status-dot"></span> <span id="status-text">Move cursor to make Panda look around! Drag to orbit 360Β°.</span>
      </div>
    </div>

    <!-- Floating Particle Overlay -->
    <div id="particle-overlay" class="particle-overlay"></div>
  </div>

  <!-- Realtime Metric Footer Bar -->
  <div class="metric-footer d-flex justify-content-between align-items-center px-3 py-2">
    <div class="metric-item"><i class="fas fa-eye text-info me-1"></i> Cursor Tracking: <strong class="text-white ms-1" id="stat-tracking">Active</strong></div>
    <div class="metric-item"><i class="fas fa-heart text-danger me-1"></i> Mood: <strong class="text-white ms-1" id="stat-mood">Playful & Hungry πŸŽ‹</strong></div>
    <div class="metric-item d-none d-sm-inline"><i class="fas fa-sync-alt text-success me-1"></i> 60 FPS WebGL Engine</div>
  </div>
</div>

B Step 2: Styling & Responsive CSS

Modern CSS flexbox and grid layouts are used to make the UI look polished, centered, and responsive across desktop, tablet, and mobile screens.

CSS (style.css)
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
  background: #090e17;
  color: #f8fafc;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
}
.panda-3d-app {
  width: 100%;
  max-width: 780px;
  background: #0f172a;
  border: 1px solid #1e293b;
  border-radius: 24px;
  overflow: hidden;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7), 0 0 40px rgba(6, 182, 212, 0.15);
  display: flex;
  flex-direction: column;
}

/* App Top Bar */
.app-top-bar {
  background: rgba(15, 23, 42, 0.85);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.bg-emerald {
  background: linear-gradient(135deg, #10b981 0%, #06b6d4 100%);
  box-shadow: 0 2px 10px rgba(6, 182, 212, 0.3);
}

/* 3D Canvas Viewport */
.viewport-wrapper {
  width: 100%;
  height: 480px;
  position: relative;
  background: radial-gradient(circle at center, #1e293b 0%, #090d16 100%);
  overflow: hidden;
  cursor: grab;
}
.viewport-wrapper:active {
  cursor: grabbing;
}
#panda-canvas {
  width: 100% !important;
  height: 100% !important;
  display: block;
}

/* Floating Action Dock */
.action-dock-container {
  position: absolute;
  bottom: 20px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  pointer-events: none;
  z-index: 10;
}
.action-dock {
  background: rgba(15, 23, 42, 0.82);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 50px;
  box-shadow: 0 15px 35px -5px rgba(0, 0, 0, 0.5);
  pointer-events: auto;
}
.btn-dock {
  background: transparent;
  color: #cbd5e1;
  border: 1px solid transparent;
  border-radius: 30px;
  padding: 6px 14px;
  font-size: 0.85rem;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  cursor: pointer;
}
.btn-dock:hover {
  background: rgba(255, 255, 255, 0.1);
  color: #ffffff;
  transform: translateY(-2px);
}
.btn-dock.active {
  background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.3);
  box-shadow: 0 4px 15px rgba(6, 182, 212, 0.4);
}
.btn-dock .emoji {
  font-size: 1.1rem;
}

/* Status Bubble Overlay */
.status-overlay {
  position: absolute;
  top: 14px;
  left: 0;
  right: 0;
  pointer-events: none;
  z-index: 10;
}
.status-bubble {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(6, 182, 212, 0.3);
  color: #e2e8f0;
  padding: 6px 16px;
  border-radius: 30px;
  font-size: 0.82rem;
  font-weight: 600;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #10b981;
  box-shadow: 0 0 8px #10b981;
  animation: pulseDot 2s infinite;
}
@keyframes pulseDot {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.4); opacity: 0.6; }
}

/* Floating Particle Canvas */
.particle-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 15;
}
.floating-particle {
  position: absolute;
  font-size: 1.6rem;
  animation: floatUp 1.6s ease-out forwards;
  pointer-events: none;
}
@keyframes floatUp {
  0% { transform: translate(0, 0) scale(0.6) rotate(0deg); opacity: 0; }
  20% { opacity: 1; }
  100% { transform: translate(var(--tx, 20px), -120px) scale(1.2) rotate(25deg); opacity: 0; }
}

/* Metric Footer */
.metric-footer {
  background: #090e17;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  font-size: 0.8rem;
  color: #94a3b8;
}
.metric-item {
  display: flex;
  align-items: center;
}

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)
const container = document.getElementById('panda-canvas-container');
const canvas = document.getElementById('panda-canvas');
const statusText = document.getElementById('status-text');
const moodText = document.getElementById('stat-mood');
const particleOverlay = document.getElementById('particle-overlay');

// Scene, Camera, Renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x131b2e);

const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 1.8, 12);

const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// Lighting Setup
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
scene.add(ambientLight);

const dirLight = new THREE.DirectionalLight(0xffffff, 0.85);
dirLight.position.set(5, 10, 7);
dirLight.castShadow = true;
scene.add(dirLight);

const pointLight = new THREE.PointLight(0x06b6d4, 1.2, 20);
pointLight.position.set(-4, 3, 4);
scene.add(pointLight);

// Materials
const whiteMat = new THREE.MeshStandardMaterial({ color: 0xfafafa, roughness: 0.5, metalness: 0.05 });
const blackMat = new THREE.MeshStandardMaterial({ color: 0x18181b, roughness: 0.6, metalness: 0.1 });
const pinkMat = new THREE.MeshStandardMaterial({ color: 0xf472b6, roughness: 0.4 });
const bambooGreenMat = new THREE.MeshStandardMaterial({ color: 0x22c55e, roughness: 0.3 });
const bambooJointMat = new THREE.MeshStandardMaterial({ color: 0x15803d, roughness: 0.4 });
const glassesMat = new THREE.MeshStandardMaterial({ color: 0x09090b, roughness: 0.1, metalness: 0.8 });
const glassLensMat = new THREE.MeshPhysicalMaterial({ color: 0x06b6d4, transmission: 0.7, opacity: 1, transparent: true, roughness: 0.1, ior: 1.5 });

// Ground Podium
const groundGeo = new THREE.CylinderGeometry(5.5, 6, 0.6, 48);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x0f172a, roughness: 0.8 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.position.y = -3.2;
ground.receiveShadow = true;
scene.add(ground);

const ringGeo = new THREE.TorusGeometry(5.6, 0.08, 16, 64);
const ringMat = new THREE.MeshBasicMaterial({ color: 0x06b6d4 });
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = Math.PI / 2;
ring.position.y = -2.9;
scene.add(ring);

// Build 3D Panda
const pandaGroup = new THREE.Group();
scene.add(pandaGroup);
pandaGroup.position.y = -0.6;

// 1. Torso
const bodyGroup = new THREE.Group();
pandaGroup.add(bodyGroup);

const bodyGeo = new THREE.SphereGeometry(2.1, 32, 32);
bodyGeo.scale(1, 1.15, 0.95);
const bodyMesh = new THREE.Mesh(bodyGeo, whiteMat);
bodyMesh.castShadow = true;
bodyGroup.add(bodyMesh);

const bellyGeo = new THREE.SphereGeometry(1.6, 32, 32);
bellyGeo.scale(0.9, 1, 0.4);
const bellyMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.6 });
const bellyMesh = new THREE.Mesh(bellyGeo, bellyMat);
bellyMesh.position.set(0, -0.2, 1.55);
bodyGroup.add(bellyMesh);

// 2. Head Group
const headGroup = new THREE.Group();
headGroup.position.set(0, 2.3, 0.2);
pandaGroup.add(headGroup);

const headGeo = new THREE.SphereGeometry(1.8, 32, 32);
headGeo.scale(1.08, 1, 0.98);
const headMesh = new THREE.Mesh(headGeo, whiteMat);
headMesh.castShadow = true;
headGroup.add(headMesh);

// Snout & Nose
const snoutGeo = new THREE.SphereGeometry(0.7, 24, 24);
snoutGeo.scale(1.1, 0.75, 0.9);
const snoutMesh = new THREE.Mesh(snoutGeo, whiteMat);
snoutMesh.position.set(0, -0.3, 1.45);
headGroup.add(snoutMesh);

const noseGeo = new THREE.SphereGeometry(0.24, 16, 16);
noseGeo.scale(1.3, 0.8, 0.8);
const noseMesh = new THREE.Mesh(noseGeo, blackMat);
noseMesh.position.set(0, -0.15, 2.05);
headGroup.add(noseMesh);

// Ears
const earGeo = new THREE.SphereGeometry(0.65, 24, 24);
earGeo.scale(1, 0.9, 0.6);
const innerEarGeo = new THREE.SphereGeometry(0.35, 16, 16);
innerEarGeo.scale(0.8, 0.8, 0.3);

const leftEarGroup = new THREE.Group();
leftEarGroup.position.set(-1.4, 1.4, -0.2);
headGroup.add(leftEarGroup);
leftEarGroup.add(new THREE.Mesh(earGeo, blackMat));
const leftInnerEar = new THREE.Mesh(innerEarGeo, pinkMat);
leftInnerEar.position.set(0, 0, 0.25);
leftEarGroup.add(leftInnerEar);

const rightEarGroup = new THREE.Group();
rightEarGroup.position.set(1.4, 1.4, -0.2);
headGroup.add(rightEarGroup);
rightEarGroup.add(new THREE.Mesh(earGeo, blackMat));
const rightInnerEar = new THREE.Mesh(innerEarGeo, pinkMat);
rightInnerEar.position.set(0, 0, 0.25);
rightEarGroup.add(rightInnerEar);

// Eye Patches
const eyePatchGeo = new THREE.SphereGeometry(0.52, 24, 24);
eyePatchGeo.scale(1, 1.3, 0.4);

const leftEyePatch = new THREE.Mesh(eyePatchGeo, blackMat);
leftEyePatch.position.set(-0.72, 0.18, 1.45);
leftEyePatch.rotation.set(0.1, -0.15, 0.35);
headGroup.add(leftEyePatch);

const rightEyePatch = new THREE.Mesh(eyePatchGeo, blackMat);
rightEyePatch.position.set(0.72, 0.18, 1.45);
rightEyePatch.rotation.set(0.1, 0.15, -0.35);
headGroup.add(rightEyePatch);

// Eyes
const eyeGeo = new THREE.SphereGeometry(0.2, 16, 16);
const pupilGeo = new THREE.SphereGeometry(0.1, 16, 16);
const shineGeo = new THREE.SphereGeometry(0.04, 12, 12);
const eyeWhiteMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const pupilMat = new THREE.MeshBasicMaterial({ color: 0x09090b });
const shineMat = new THREE.MeshBasicMaterial({ color: 0xffffff });

const leftEyeGroup = new THREE.Group();
leftEyeGroup.position.set(-0.68, 0.2, 1.62);
headGroup.add(leftEyeGroup);
leftEyeGroup.add(new THREE.Mesh(eyeGeo, eyeWhiteMat));
const leftPupil = new THREE.Mesh(pupilGeo, pupilMat);
leftPupil.position.set(0, 0, 0.12);
leftEyeGroup.add(leftPupil);
const leftShine = new THREE.Mesh(shineGeo, shineMat);
leftShine.position.set(0.04, 0.04, 0.18);
leftEyeGroup.add(leftShine);

const rightEyeGroup = new THREE.Group();
rightEyeGroup.position.set(0.68, 0.2, 1.62);
headGroup.add(rightEyeGroup);
rightEyeGroup.add(new THREE.Mesh(eyeGeo, eyeWhiteMat));
const rightPupil = new THREE.Mesh(pupilGeo, pupilMat);
rightPupil.position.set(0, 0, 0.12);
rightEyeGroup.add(rightPupil);
const rightShine = new THREE.Mesh(shineGeo, shineMat);
rightShine.position.set(0.04, 0.04, 0.18);
rightEyeGroup.add(rightShine);

// Sunglasses Group
const sunglassesGroup = new THREE.Group();
sunglassesGroup.position.set(0, 0.25, 1.75);
sunglassesGroup.visible = false;
headGroup.add(sunglassesGroup);

const frameGeo = new THREE.BoxGeometry(2.1, 0.45, 0.12);
const leftLensGeo = new THREE.CylinderGeometry(0.42, 0.42, 0.1, 24);
leftLensGeo.rotateX(Math.PI / 2);
sunglassesGroup.add(new THREE.Mesh(frameGeo, glassesMat));
const leftLens = new THREE.Mesh(leftLensGeo, glassLensMat);
leftLens.position.set(-0.65, 0, 0.02);
sunglassesGroup.add(leftLens);
const rightLens = new THREE.Mesh(leftLensGeo.clone(), glassLensMat);
rightLens.position.set(0.65, 0, 0.02);
sunglassesGroup.add(rightLens);

// 3. Arms
const armGeo = new THREE.SphereGeometry(0.7, 24, 24);
armGeo.scale(0.85, 1.8, 0.85);

const leftArmGroup = new THREE.Group();
leftArmGroup.position.set(-1.85, 0.6, 0.2);
pandaGroup.add(leftArmGroup);
const leftArmMesh = new THREE.Mesh(armGeo, blackMat);
leftArmMesh.position.set(0, -0.7, 0);
leftArmMesh.rotation.z = 0.25;
leftArmGroup.add(leftArmMesh);

const rightArmGroup = new THREE.Group();
rightArmGroup.position.set(1.85, 0.6, 0.2);
pandaGroup.add(rightArmGroup);
const rightArmMesh = new THREE.Mesh(armGeo, blackMat);
rightArmMesh.position.set(0, -0.7, 0);
rightArmMesh.rotation.z = -0.25;
rightArmGroup.add(rightArmMesh);

// 4. Legs
const legGeo = new THREE.SphereGeometry(0.85, 24, 24);
legGeo.scale(1, 0.8, 1.4);
const leftLeg = new THREE.Mesh(legGeo, blackMat);
leftLeg.position.set(-1.25, -2.1, 0.6);
leftLeg.rotation.set(-0.15, 0.25, 0);
pandaGroup.add(leftLeg);

const rightLeg = new THREE.Mesh(legGeo, blackMat);
rightLeg.position.set(1.25, -2.1, 0.6);
rightLeg.rotation.set(-0.15, -0.25, 0);
pandaGroup.add(rightLeg);

// 5. Bamboo
const bambooGroup = new THREE.Group();
bambooGroup.position.set(1.1, 0.4, 1.4);
bambooGroup.rotation.set(0.3, -0.2, 0.2);
pandaGroup.add(bambooGroup);

for (let i = 0; i < 4; i++) {
  const segmentGeo = new THREE.CylinderGeometry(0.12, 0.12, 0.8, 16);
  const segment = new THREE.Mesh(segmentGeo, bambooGreenMat);
  segment.position.y = i * 0.85;
  bambooGroup.add(segment);
  const jointGeo = new THREE.TorusGeometry(0.14, 0.03, 8, 16);
  const joint = new THREE.Mesh(jointGeo, bambooJointMat);
  joint.rotation.x = Math.PI / 2;
  joint.position.y = i * 0.85 + 0.4;
  bambooGroup.add(joint);
}

// Interaction state
let mouse = { x: 0, y: 0, targetX: 0, targetY: 0 };
let currentAction = 'feed';
let isDragging = false;
let prevMousePos = { x: 0, y: 0 };

function createParticle(type = 'heart') {
  const p = document.createElement('div');
  p.className = 'floating-particle';
  p.textContent = type === 'heart' ? 'πŸ’–' : type === 'bamboo' ? 'πŸŽ‹' : type === 'zzz' ? 'πŸ’€' : '✨';
  p.style.left = (container.clientWidth / 2 + (Math.random() - 0.5) * 120) + 'px';
  p.style.top = (container.clientHeight / 2 - 40 + (Math.random() - 0.5) * 60) + 'px';
  particleOverlay.appendChild(p);
  setTimeout(() => p.remove(), 1600);
}

window.addEventListener('mousemove', (e) => {
  const rect = container.getBoundingClientRect();
  const rawX = e.clientX - rect.left;
  const rawY = e.clientY - rect.top;
  if (rawX >= 0 && rawX <= rect.width && rawY >= 0 && rawY <= rect.height) {
    mouse.targetX = (rawX / rect.width) * 2 - 1;
    mouse.targetY = -(rawY / rect.height) * 2 + 1;
  }
});

canvas.addEventListener('mousedown', (e) => {
  isDragging = true;
  prevMousePos = { x: e.clientX, y: e.clientY };
});
window.addEventListener('mouseup', () => isDragging = false);
window.addEventListener('mousemove', (e) => {
  if (isDragging) {
    const deltaX = e.clientX - prevMousePos.x;
    pandaGroup.rotation.y += deltaX * 0.01;
    prevMousePos = { x: e.clientX, y: e.clientY };
  }
});

// Action Dock
const actionBtns = document.querySelectorAll('.btn-dock');
actionBtns.forEach(btn => {
  btn.addEventListener('click', () => {
    actionBtns.forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    const act = btn.dataset.action;

    if (act === 'glasses') {
      sunglassesGroup.visible = !sunglassesGroup.visible;
      statusText.textContent = sunglassesGroup.visible ? 'Looking stylish with cool sunglasses! πŸ•ΆοΈ' : 'Took off sunglasses.';
      moodText.textContent = sunglassesGroup.visible ? 'Super Cool 😎' : 'Happy & Playful';
      return;
    }

    currentAction = act;

    if (act === 'feed') {
      statusText.textContent = 'Munching tasty fresh bamboo! πŸŽ‹ Nom nom...';
      moodText.textContent = 'Eating Bamboo πŸŽ‹';
      for (let i = 0; i < 4; i++) setTimeout(() => createParticle('bamboo'), i * 300);
    } else if (act === 'wave') {
      statusText.textContent = 'Waving hello to you! πŸ‘‹ Greetings friend!';
      moodText.textContent = 'Friendly & Waving πŸ‘‹';
      for (let i = 0; i < 3; i++) setTimeout(() => createParticle('heart'), i * 350);
    } else if (act === 'dance') {
      statusText.textContent = 'Party time! Panda is dancing happily πŸ•Ίβœ¨';
      moodText.textContent = 'Dancing Joyfully πŸ•Ί';
      for (let i = 0; i < 6; i++) setTimeout(() => createParticle('sparkle'), i * 250);
    } else if (act === 'sleep') {
      statusText.textContent = 'Taking a peaceful power nap... Zzz 😴';
      moodText.textContent = 'Sleeping / Resting πŸ’€';
      for (let i = 0; i < 4; i++) setTimeout(() => createParticle('zzz'), i * 400);
    }
  });
});

// Lighting Themes
const themeDay = document.getElementById('theme-day');
const themeSunset = document.getElementById('theme-sunset');
const themeNight = document.getElementById('theme-night');
const themeBtns = [themeDay, themeSunset, themeNight];

themeDay.addEventListener('click', () => setTheme(themeDay, 0x131b2e, 0xffffff, 0x06b6d4, 0.85));
themeSunset.addEventListener('click', () => setTheme(themeSunset, 0x2e1065, 0xf472b6, 0xfbbf24, 0.95));
themeNight.addEventListener('click', () => setTheme(themeNight, 0x05070f, 0x38bdf8, 0xec4899, 1.2));

function setTheme(activeBtn, bgColor, dirColor, pointColor, intensity) {
  themeBtns.forEach(b => b.classList.remove('active'));
  activeBtn.classList.add('active');
  scene.background.setHex(bgColor);
  dirLight.color.setHex(dirColor);
  pointLight.color.setHex(pointColor);
  pointLight.intensity = intensity;
  ringMat.color.setHex(pointColor);
}

document.getElementById('btn-reset-cam').addEventListener('click', () => {
  camera.position.set(0, 1.8, 12);
  pandaGroup.rotation.set(0, 0, 0);
  statusText.textContent = 'Camera reset to front view.';
});

// Animation Loop
let clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const time = clock.getElapsedTime();

  mouse.x += (mouse.targetX - mouse.x) * 0.08;
  mouse.y += (mouse.targetY - mouse.y) * 0.08;

  if (currentAction !== 'sleep') {
    headGroup.rotation.y = mouse.x * 0.5;
    headGroup.rotation.x = -mouse.y * 0.35;
    leftPupil.position.x = mouse.x * 0.06;
    leftPupil.position.y = mouse.y * 0.06;
    rightPupil.position.x = mouse.x * 0.06;
    rightPupil.position.y = mouse.y * 0.06;
  }

  bodyGroup.scale.y = 1 + Math.sin(time * 2.5) * 0.02;

  if (currentAction === 'feed') {
    bambooGroup.position.set(0.3, 1.8 + Math.sin(time * 8) * 0.08, 1.5);
    bambooGroup.rotation.set(0.4, 0.2, 0.4);
    rightArmGroup.rotation.x = 0.8 + Math.sin(time * 8) * 0.1;
    snoutMesh.scale.y = 0.75 + Math.sin(time * 12) * 0.08;
  } else if (currentAction === 'wave') {
    rightArmGroup.rotation.x = 0.2;
    rightArmGroup.rotation.z = 1.4 + Math.sin(time * 8) * 0.35;
    leftEarGroup.rotation.z = Math.sin(time * 8) * 0.15;
  } else if (currentAction === 'dance') {
    pandaGroup.position.y = -0.6 + Math.abs(Math.sin(time * 6)) * 0.4;
    pandaGroup.rotation.y += Math.sin(time * 4) * 0.03;
    leftArmGroup.rotation.z = -0.8 + Math.sin(time * 6) * 0.4;
    rightArmGroup.rotation.z = 0.8 - Math.sin(time * 6) * 0.4;
    leftEarGroup.rotation.z = Math.sin(time * 10) * 0.3;
    rightEarGroup.rotation.z = -Math.sin(time * 10) * 0.3;
  } else if (currentAction === 'sleep') {
    headGroup.rotation.x = 0.4 + Math.sin(time * 1.2) * 0.05;
    headGroup.rotation.y = 0;
  }

  ring.rotation.z = time * 0.2;
  renderer.render(scene, camera);
}

function onResize() {
  if (!container || !renderer || !camera) return;
  const width = container.clientWidth;
  const height = container.clientHeight;
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
}
window.addEventListener('resize', onResize);

onResize();
animate();

3. Core JavaScript Concepts You Learned

Three.js Scene, Camera & WebGLRenderer

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

Procedural 3D Geometry Construction

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

Smooth Mouse Pointer Interpolation (Lerp)

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

Interactive 3D Animation State Machine

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

Bootstrap 5 Floating UI Integration

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

Lighting Studio & Theme Switching

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 interactive-3d-panda-project.zip.
  2. Extract the archive: Unzip the downloaded file. Inside you will find separate index.html, style.css, script.js, index-standalone.html, and a comprehensive README.md.
  3. Open in any browser: Simply double-click index.html (or index-standalone.html) to open and test immediately in Chrome, Firefox, Safari, or Edge.
  4. Edit in Visual Studio Code: Open the extracted folder in VS Code. Right-click index.html and select "Open with Live Server" to enjoy instant live reloading as you modify HTML, CSS, or JS!

Bonus Challenges for Learners

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

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

Other Beginner Projects You Might Like

AnimatedBeginner20 mins
UI & Animations

Interactive Animated Login Form

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

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

Light Switch & Lamp Turn On Login Form

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

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

Folding Paper Plane Subscribe Animation

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

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