Web Animations Medium
3D Product Showcase
Interactive 3D product viewer with PBR materials, three-point lighting, and orbit controls.
Open in Lab
MCP
three.js pbr-material orbit
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #050508;
color: #f0f4fb;
font-family: "Inter", "SF Pro Display", system-ui, sans-serif;
overflow: hidden;
height: 100vh;
}
#canvas-container {
position: fixed;
inset: 0;
z-index: 0;
}
#canvas-container canvas {
display: block;
width: 100%;
height: 100%;
}
.overlay {
position: fixed;
inset: 0;
z-index: 2;
display: flex;
align-items: flex-end;
padding: 3rem;
pointer-events: none;
}
.info-panel {
pointer-events: auto;
max-width: 340px;
background: rgba(12, 15, 25, 0.7);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(134, 232, 255, 0.1);
border-radius: 18px;
padding: 2rem;
}
.eyebrow {
display: inline-block;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
color: #86e8ff;
margin-bottom: 0.6rem;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 0.82rem;
color: #8a95a8;
line-height: 1.5;
margin-bottom: 1.2rem;
}
.specs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-bottom: 1.2rem;
}
.spec {
background: rgba(134, 232, 255, 0.05);
border-radius: 8px;
padding: 0.6rem 0.8rem;
}
.spec-label {
display: block;
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #8a95a8;
margin-bottom: 0.15rem;
}
.spec-value {
font-size: 0.85rem;
font-weight: 600;
color: #86e8ff;
}
.controls {
display: flex;
gap: 0.5rem;
}
.ctrl-btn {
flex: 1;
padding: 0.55rem;
border-radius: 8px;
border: 1px solid rgba(134, 232, 255, 0.2);
background: rgba(134, 232, 255, 0.06);
color: #86e8ff;
font: 600 0.75rem / 1 "Inter", system-ui, sans-serif;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.ctrl-btn:hover {
background: rgba(134, 232, 255, 0.12);
border-color: #86e8ff;
}
.btn-back {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 10;
padding: 0.6rem 1.5rem;
border-radius: 999px;
border: 1px solid rgba(134, 232, 255, 0.2);
color: #86e8ff;
text-decoration: none;
font: 600 0.8rem / 1 "Inter", system-ui, sans-serif;
background: rgba(5, 5, 8, 0.7);
backdrop-filter: blur(8px);
transition: all 0.25s;
}
.btn-back:hover {
background: rgba(134, 232, 255, 0.08);
border-color: #86e8ff;
}
@media (max-width: 640px) {
.overlay {
padding: 1.5rem;
align-items: flex-end;
}
.info-panel {
max-width: 100%;
}
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import * as THREE from "three";
initDemoShell({
title: "3D Product Showcase",
category: "3d",
tech: ["three.js", "pbr-material", "lighting"],
});
let reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add("reduced-motion");
window.addEventListener("motion-preference", (e) => {
reduced = e.detail.reduced;
});
// State
let orbiting = true;
let colorIndex = 0;
const colors = [
new THREE.Color("#86e8ff"),
new THREE.Color("#ae52ff"),
new THREE.Color("#ff40d6"),
new THREE.Color("#ffcc66"),
new THREE.Color("#50c878"),
];
// Scene
const container = document.getElementById("canvas-container");
const scene = new THREE.Scene();
scene.background = new THREE.Color("#050508");
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 1, 6);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
container.appendChild(renderer.domElement);
// Lights — three-point setup
const keyLight = new THREE.DirectionalLight(0xffffff, 2.5);
keyLight.position.set(5, 5, 5);
scene.add(keyLight);
const fillLight = new THREE.DirectionalLight(0x86e8ff, 1.0);
fillLight.position.set(-4, 2, -3);
scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0xae52ff, 1.5);
rimLight.position.set(0, -3, -5);
scene.add(rimLight);
scene.add(new THREE.AmbientLight(0x111122, 0.5));
// Object — Torus Knot
const geometry = new THREE.TorusKnotGeometry(1.2, 0.4, 200, 32);
const material = new THREE.MeshPhysicalMaterial({
color: colors[0],
metalness: 0.9,
roughness: 0.12,
clearcoat: 1.0,
clearcoatRoughness: 0.05,
reflectivity: 1.0,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Ground reflection plane (subtle)
const planeGeo = new THREE.PlaneGeometry(20, 20);
const planeMat = new THREE.MeshStandardMaterial({
color: 0x050508,
metalness: 0.8,
roughness: 0.3,
});
const plane = new THREE.Mesh(planeGeo, planeMat);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -2;
scene.add(plane);
// Mouse
const mouse = { x: 0, y: 0, sx: 0, sy: 0 };
document.addEventListener("mousemove", (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
// Controls
document.getElementById("btn-pause").addEventListener("click", () => {
orbiting = !orbiting;
document.getElementById("btn-pause").textContent = orbiting ? "Pause Orbit" : "Resume Orbit";
});
document.getElementById("btn-color").addEventListener("click", () => {
colorIndex = (colorIndex + 1) % colors.length;
material.color.copy(colors[colorIndex]);
});
// Animation
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
if (!reduced) {
// Smooth mouse
mouse.sx += (mouse.x - mouse.sx) * 0.03;
mouse.sy += (mouse.y - mouse.sy) * 0.03;
if (orbiting) {
mesh.rotation.y = t * 0.4;
mesh.rotation.x = Math.sin(t * 0.2) * 0.15;
}
// Mouse influence on camera
camera.position.x = mouse.sx * 1.5;
camera.position.y = 1 + mouse.sy * 0.8;
camera.lookAt(0, 0, 0);
// Floating bob
mesh.position.y = Math.sin(t * 0.8) * 0.1;
}
renderer.render(scene, camera);
}
animate();
// Resize
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
window.addEventListener("beforeunload", () => {
geometry.dispose();
material.dispose();
renderer.dispose();
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Product Showcase — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"https://esm.sh/gsap@3.13.0","gsap/ScrollTrigger":"https://esm.sh/gsap@3.13.0/ScrollTrigger","gsap/SplitText":"https://esm.sh/gsap@3.13.0/SplitText","gsap/Flip":"https://esm.sh/gsap@3.13.0/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/gsap@3.13.0/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/gsap@3.13.0/TextPlugin","gsap/all":"https://esm.sh/gsap@3.13.0/all","gsap/":"https://esm.sh/gsap@3.13.0/","lenis":"https://esm.sh/lenis@1.1.13/dist/lenis.mjs","three":"https://esm.sh/three@0.171.0","three/addons/":"https://esm.sh/three@0.171.0/examples/jsm/"}}</script>
</head>
<body>
<div id="canvas-container" aria-hidden="true"></div>
<div class="overlay">
<div class="info-panel">
<span class="eyebrow">Demo 08</span>
<h1>3D Product Showcase</h1>
<p class="subtitle">A procedural torus knot with MeshPhysicalMaterial — metalness, clearcoat, and three-point lighting. Click to pause/resume orbit.</p>
<div class="specs">
<div class="spec"><span class="spec-label">Material</span><span class="spec-value">Physical (PBR)</span></div>
<div class="spec"><span class="spec-label">Metalness</span><span class="spec-value">0.9</span></div>
<div class="spec"><span class="spec-label">Clearcoat</span><span class="spec-value">1.0</span></div>
<div class="spec"><span class="spec-label">Roughness</span><span class="spec-value">0.12</span></div>
</div>
<div class="controls">
<button id="btn-pause" class="ctrl-btn">Pause Orbit</button>
<button id="btn-color" class="ctrl-btn">Cycle Color</button>
</div>
</div>
</div>
<a href="/" class="btn-back">Back to Showcase</a>
<script type="module" src="script.js"></script>
</body>
</html>3D Product Showcase
Interactive 3D product viewer with PBR materials, three-point lighting, and orbit controls.
Source
- Repository:
libs-genclaude - Original demo id:
08-product-showcase
Notes
Interactive 3D product viewer with PBR materials, three-point lighting, and orbit controls.