<!DOCTYPE html>
<html lang="en" class="h-full dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive 3D Product Configurator - WebGL App</title>
<!-- Fonts & Icons -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Tailwind CSS & Three.js CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
outfit: ['Outfit', 'sans-serif']
}
}
}
}
</script>
<style>
body { margin: 0; padding: 0; height: 100vh; overflow: hidden; background-color: #090d16; }
#canvas3d { width: 100%; height: 100%; cursor: grab; }
#canvas3d:active { cursor: grabbing; }
</style>
</head>
<body class="h-full text-slate-100 flex flex-col font-sans select-none">
<!-- Header Navigation Toolbar -->
<header class="h-14 bg-slate-900/90 backdrop-blur-md border-b border-slate-800 px-4 flex items-center justify-between shrink-0 z-30">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-cyan-500 to-blue-600 flex items-center justify-center text-slate-950 font-outfit font-extrabold text-base shadow-lg shadow-cyan-500/20">
🧊
</div>
<div>
<h1 class="font-outfit font-bold text-sm text-white tracking-wide">Interactive 3D Product Configurator</h1>
<p class="text-[10px] text-cyan-400 font-mono">WebGL Three.js 360° Material Visualizer</p>
</div>
</div>
<div class="flex items-center gap-2">
<button id="resetCamBtn" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-outfit font-bold text-xs transition flex items-center gap-1.5 border border-slate-700">
<i class="fa-solid fa-rotate-left"></i>
<span>Reset View</span>
</button>
<button id="takeSnapshotBtn" class="px-3.5 py-1.5 rounded-lg bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-outfit font-bold text-xs transition flex items-center gap-1.5 shadow-md shadow-cyan-500/20">
<i class="fa-solid fa-camera"></i>
<span>Snapshot PNG</span>
</button>
</div>
</header>
<!-- Main Workspace -->
<div class="flex-1 flex flex-col md:flex-row relative overflow-hidden">
<!-- Left 3D Viewport -->
<div class="flex-1 relative bg-slate-950 overflow-hidden">
<canvas id="canvas3d"></canvas>
<!-- Floating 360 Badge Overlay -->
<div class="absolute bottom-4 left-4 bg-slate-900/80 backdrop-blur-md border border-slate-800 rounded-2xl px-3.5 py-2 flex items-center gap-2.5 text-xs text-slate-300 pointer-events-none">
<span class="w-2.5 h-2.5 rounded-full bg-cyan-400 animate-ping"></span>
<span class="font-outfit font-bold">360° Drag to Rotate • Scroll to Zoom</span>
</div>
</div>
<!-- Right Control Panel -->
<div class="w-full md:w-80 bg-slate-900/90 backdrop-blur-md border-t md:border-t-0 md:border-l border-slate-800 p-5 flex flex-col justify-between shrink-0 space-y-6 overflow-y-auto z-20">
<div class="space-y-6">
<!-- Color Swatch Customization -->
<div class="space-y-3">
<label class="block font-outfit font-bold text-xs uppercase tracking-wider text-cyan-400">
🎨 Material Finish Color
</label>
<div class="grid grid-cols-4 gap-2.5">
<button onclick="changeColor(0x06b6d4, 'Cyber Cyan')" class="color-btn w-full h-10 rounded-xl bg-cyan-500 border-2 border-white/80 shadow-lg shadow-cyan-500/30 hover:scale-105 transition" title="Cyber Cyan"></button>
<button onclick="changeColor(0xa855f7, 'Neon Purple')" class="color-btn w-full h-10 rounded-xl bg-purple-500 border-2 border-transparent hover:border-white/80 hover:scale-105 transition" title="Neon Purple"></button>
<button onclick="changeColor(0x10b981, 'Emerald Green')" class="color-btn w-full h-10 rounded-xl bg-emerald-500 border-2 border-transparent hover:border-white/80 hover:scale-105 transition" title="Emerald Green"></button>
<button onclick="changeColor(0xf59e0b, 'Sunset Gold')" class="color-btn w-full h-10 rounded-xl bg-amber-500 border-2 border-transparent hover:border-white/80 hover:scale-105 transition" title="Sunset Gold"></button>
</div>
<div class="text-[11px] text-slate-400 font-mono">Selected: <span id="activeColorName" class="text-white font-bold">Cyber Cyan</span></div>
</div>
<!-- Material Reflectivity & Metalness Sliders -->
<div class="space-y-3">
<label class="block font-outfit font-bold text-xs uppercase tracking-wider text-cyan-400">
⚙️ Surface Physics Properties
</label>
<div class="space-y-1.5">
<div class="flex justify-between text-xs font-semibold text-slate-300">
<span>Metalness / Gloss</span>
<span id="metalnessVal" class="font-mono text-cyan-400">0.85</span>
</div>
<input type="range" id="metalnessInput" min="0" max="1" step="0.05" value="0.85" class="w-full accent-cyan-500">
</div>
<div class="space-y-1.5">
<div class="flex justify-between text-xs font-semibold text-slate-300">
<span>Roughness</span>
<span id="roughnessVal" class="font-mono text-cyan-400">0.20</span>
</div>
<input type="range" id="roughnessInput" min="0" max="1" step="0.05" value="0.20" class="w-full accent-cyan-500">
</div>
</div>
<!-- Lighting Studio Environment -->
<div class="space-y-3">
<label class="block font-outfit font-bold text-xs uppercase tracking-wider text-cyan-400">
💡 Studio Environment Lighting
</label>
<select id="lightSelect" class="w-full bg-slate-950 border border-slate-800 text-xs font-semibold text-slate-200 rounded-xl px-3 py-2.5 focus:outline-none focus:border-cyan-500">
<option value="cyber">Cyber Neon Studio</option>
<option value="warm">Warm Sunset Lighting</option>
<option value="clean">Clean High-Key White</option>
</select>
</div>
<!-- Rotation Speed Toggle -->
<div class="space-y-2">
<label class="inline-flex items-center gap-2 text-xs font-semibold text-slate-300 cursor-pointer">
<input type="checkbox" id="autoRotateToggle" checked class="rounded border-slate-700 text-cyan-500 focus:ring-0">
<span>Auto-Orbit 360° Rotation</span>
</label>
</div>
</div>
<div class="p-3 bg-slate-950/80 border border-slate-800 rounded-2xl text-[11px] text-slate-400 space-y-1">
<div class="text-white font-bold">Model Spec:</div>
<div>Polycount: 12,480 Triangles</div>
<div>Render Engine: WebGL 2.0 Shader</div>
</div>
</div>
</div>
<!-- Three.js Script Logic -->
<script>
const canvas = document.getElementById('canvas3d');
const container = canvas.parentElement;
// Scene, Camera, Renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x090d16);
const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, preserveDrawingBuffer: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
// Lighting Setup
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const mainLight = new THREE.DirectionalLight(0x06b6d4, 1.5);
mainLight.position.set(5, 5, 5);
mainLight.castShadow = true;
scene.add(mainLight);
const fillLight = new THREE.PointLight(0xa855f7, 1.2);
fillLight.position.set(-5, -2, -5);
scene.add(fillLight);
// Create 3D Product Mesh (Futuristic Sci-Fi Gadget Core)
const group = new THREE.Group();
// Core Sphere
const coreGeo = new THREE.IcosahedronGeometry(1, 4);
const mainMaterial = new THREE.MeshStandardMaterial({
color: 0x06b6d4,
metalness: 0.85,
roughness: 0.2,
envMapIntensity: 1.0
});
const coreMesh = new THREE.Mesh(coreGeo, mainMaterial);
coreMesh.castShadow = true;
group.add(coreMesh);
// Outer Ring 1
const ringGeo1 = new THREE.TorusGeometry(1.4, 0.06, 16, 100);
const ringMat1 = new THREE.MeshStandardMaterial({ color: 0x334155, metalness: 0.9, roughness: 0.1 });
const ring1 = new THREE.Mesh(ringGeo1, ringMat1);
ring1.rotation.x = Math.PI / 3;
group.add(ring1);
// Outer Ring 2
const ringGeo2 = new THREE.TorusGeometry(1.7, 0.04, 16, 100);
const ringMat2 = new THREE.MeshStandardMaterial({ color: 0x06b6d4, metalness: 0.95, roughness: 0.05 });
const ring2 = new THREE.Mesh(ringGeo2, ringMat2);
ring2.rotation.y = Math.PI / 4;
group.add(ring2);
scene.add(group);
// Orbit Drag Control Logic
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
const autoRotateToggle = document.getElementById('autoRotateToggle');
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
previousMousePosition = { x: e.clientX, y: e.clientY };
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaMove = { x: e.clientX - previousMousePosition.x, y: e.clientY - previousMousePosition.y };
group.rotation.y += deltaMove.x * 0.008;
group.rotation.x += deltaMove.y * 0.008;
previousMousePosition = { x: e.clientX, y: e.clientY };
});
window.addEventListener('mouseup', () => isDragging = false);
// Zoom Control
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
camera.position.z = Math.max(3, Math.min(10, camera.position.z + e.deltaY * 0.003));
});
// Animation Loop
function animate() {
requestAnimationFrame(animate);
if (autoRotateToggle.checked && !isDragging) {
group.rotation.y += 0.006;
ring1.rotation.z += 0.004;
ring2.rotation.z -= 0.005;
}
renderer.render(scene, camera);
}
animate();
// Responsive Resize
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
// UI Controls Events
function changeColor(hex, name) {
mainMaterial.color.setHex(hex);
ringMat2.color.setHex(hex);
document.getElementById('activeColorName').innerText = name;
}
const metalnessInput = document.getElementById('metalnessInput');
const roughnessInput = document.getElementById('roughnessInput');
metalnessInput.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
mainMaterial.metalness = val;
document.getElementById('metalnessVal').innerText = val.toFixed(2);
});
roughnessInput.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
mainMaterial.roughness = val;
document.getElementById('roughnessVal').innerText = val.toFixed(2);
});
document.getElementById('lightSelect').addEventListener('change', (e) => {
const val = e.target.value;
if (val === 'cyber') {
mainLight.color.setHex(0x06b6d4);
fillLight.color.setHex(0xa855f7);
} else if (val === 'warm') {
mainLight.color.setHex(0xf59e0b);
fillLight.color.setHex(0xf43f5e);
} else {
mainLight.color.setHex(0xffffff);
fillLight.color.setHex(0xffffff);
}
});
document.getElementById('resetCamBtn').addEventListener('click', () => {
camera.position.set(0, 2, 5);
group.rotation.set(0, 0, 0);
});
document.getElementById('takeSnapshotBtn').addEventListener('click', () => {
renderer.render(scene, camera);
const dataUrl = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = dataUrl;
a.download = '3d-product-snapshot.png';
a.click();
});
</script>
</body>
</html>
/* Supports both Dark and Light mode via Tailwind class variant */
// Interactive scripts included inside demo view file