text
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BODYX - 3D Virtual Showroom</title>
<style>
/* --- 基础页面样式 (模拟你网页的极简风格) --- */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #ffffff;
color: #333;
overflow: hidden; /* 隐藏滚动条,专注于3D体验 */
}
/* 顶部导航模拟 (简化版) */
.navbar {
position: absolute;
top: 0; left: 0; right: 0;
height: 70px;
display: flex;
align-items: center;
padding: 0 40px;
border-bottom: 1px solid #eee;
z-index: 10;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
}
.logo { font-weight: 900; font-size: 24px; font-style: italic; letter-spacing: -1px; }
.logo span { color: #e11d48; } /* 品牌红 */
/* --- 3D 展厅容器 --- */
#showroom-container {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
/* --- 交互控制台 (底部悬浮) --- */
.control-panel {
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
width: 600px;
max-width: 90%;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
padding: 24px 32px;
z-index: 10;
transition: all 0.3s ease;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.panel-title {
font-size: 16px;
font-weight: 600;
color: #111;
display: flex;
align-items: center;
}
.panel-title::before {
content: '';
display: inline-block;
width: 8px; height: 8px;
background: #e11d48; /* 品牌红 */
border-radius: 50%;
margin-right: 10px;
}
.panel-value {
font-size: 14px;
color: #e11d48; /* 品牌红 */
font-weight: bold;
}
/* 自定义滑块样式 */
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
margin: 10px 0;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #f3f4f6;
border-radius: 2px;
}
input[type=range]::-webkit-slider-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
background: #ffffff;
border: 2px solid #e11d48;
cursor: pointer;
-webkit-appearance: none;
margin-top: -8px;
box-shadow: 0 2px 6px rgba(225, 29, 72, 0.2);
transition: transform 0.1s;
}
input[type=range]::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
/* 提示文字 */
.hint-text {
text-align: center;
font-size: 12px;
color: #9ca3af;
margin-top: 15px;
}
/* 器材标签 (3D空间映射到2D) */
.tooltip {
position: absolute;
background: #111;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
transform: translate(-50%, -100%);
margin-top: -15px;
z-index: 5;
white-space: nowrap;
}
.tooltip.active {
opacity: 1;
}
.tooltip::after {
content: '';
position: absolute;
bottom: -4px; left: 50%;
transform: translateX(-50%);
border-width: 5px 5px 0;
border-style: solid;
border-color: #111 transparent transparent transparent;
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="navbar">
<div class="logo">BODY<span>X</span></div>
</div>
<!-- 3D 渲染区 -->
<div id="showroom-container"></div>
<!-- 交互控制台 -->
<div class="control-panel">
<div class="panel-header">
<div class="panel-title" id="sliderLabel">阻力级别 (Resistance)</div>
<div class="panel-value" id="sliderValue">50</div>
</div>
<!-- 滑块 -->
<input type="range" id="paramSlider" min="0" max="100" value="50">
<div class="hint-text">滑动滑块,视角自动漫游聚焦符合参数的器材</div>
</div>
<!-- 引入 Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
// --- 1. 基础场景设置 ---
const container = document.getElementById('showroom-container');
const scene = new THREE.Scene();
// 背景色:极简浅灰白
scene.background = new THREE.Color(0xf8f9fa);
// 添加一点雾化效果,让空间有深邃感
scene.fog = new THREE.FogExp2(0xf8f9fa, 0.015);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 15, 35); // 初始高视角
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 - 0.05; // 防止镜头穿入地下
controls.enablePan = false; // 限制平移,保持中心视觉
// --- 2. 灯光设置 (极简风格需要干净的阴影) ---
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(10, 20, 10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -20;
dirLight.shadow.camera.right = 20;
dirLight.shadow.camera.top = 20;
dirLight.shadow.camera.bottom = -20;
scene.add(dirLight);
// 补光,增加金属光泽
const fillLight = new THREE.DirectionalLight(0xe11d48, 0.3); // 微弱的品牌红补光
fillLight.position.set(-10, 5, -10);
scene.add(fillLight);
// --- 3. 空间与地面 ---
// 创建极简的圆形展台
const floorGeometry = new THREE.CylinderGeometry(25, 25, 0.5, 64);
const floorMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.1,
metalness: 0.1
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.position.y = -0.25;
floor.receiveShadow = true;
scene.add(floor);
// 添加一些极简的网格辅助线增加科技感(可选,如果不想要可以注释掉)
const gridHelper = new THREE.GridHelper(50, 50, 0xe5e7eb, 0xe5e7eb);
gridHelper.position.y = 0.01;
scene.add(gridHelper);
// --- 4. 生成虚拟器材 (使用几何体拼凑替代真实模型) ---
const equipmentData = [
{ name: '智能动感单车', type: 'bike', x: -8, z: -5, res: 20, int: 30, color: 0x333333 },
{ name: '旗舰综合训练架', type: 'rack', x: 0, z: 0, res: 80, int: 90, color: 0x111111 }, // 中央主推
{ name: '轻量化跑步机', type: 'treadmill', x: 8, z: -2, res: 50, int: 60, color: 0x555555 },
{ name: '高透风划船机', type: 'rower', x: -5, z: 8, res: 40, int: 80, color: 0x222222 },
{ name: '智能哑铃架', type: 'dumbbell', x: 7, z: 7, res: 90, int: 40, color: 0x444444 }
];
const equipmentMeshes = [];
const redMaterial = new THREE.MeshStandardMaterial({ color: 0xe11d48, roughness: 0.3, metalness: 0.8 });
const darkMetal = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5, metalness: 0.6 });
const lightMetal = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.2, metalness: 0.9 });
const glassMaterial = new THREE.MeshPhysicalMaterial({ color: 0xffffff, transmission: 0.9, opacity: 1, roughness: 0.1, metalness: 0, ior: 1.5, thickness: 0.5 });
function createEquipment(data, index) {
const group = new THREE.Group();
group.position.set(data.x, 0, data.z);
group.userData = data;
group.userData.index = index;
// 根据类型生成简单几何体组合
if (data.type === 'bike') {
// 飞轮
const wheel = new THREE.Mesh(new THREE.CylinderGeometry(1.5, 1.5, 0.3, 32), redMaterial);
wheel.rotation.z = Math.PI / 2;
wheel.position.set(0, 1.5, -0.5);
wheel.castShadow = true;
group.add(wheel);
// 车架
const frame = new THREE.Mesh(new THREE.BoxGeometry(0.2, 2.5, 0.2), lightMetal);
frame.position.set(0, 1.25, 0.5);
frame.castShadow = true;
group.add(frame);
// 座椅
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.2, 0.8), darkMetal);
seat.position.set(0, 2.5, 0.5);
seat.castShadow = true;
group.add(seat);
} else if (data.type === 'rack') {
// 综合训练架 (中央旗舰,做大一点)
const base = new THREE.Mesh(new THREE.BoxGeometry(4, 0.2, 4), darkMetal);
base.position.y = 0.1;
base.receiveShadow = true;
group.add(base);
// 四根立柱
const pillarGeo = new THREE.BoxGeometry(0.3, 5, 0.3);
const positions = [[-1.8, 2.5, -1.8], [1.8, 2.5, -1.8], [-1.8, 2.5, 1.8], [1.8, 2.5, 1.8]];
positions.forEach(pos => {
const pillar = new THREE.Mesh(pillarGeo, lightMetal);
pillar.position.set(...pos);
pillar.castShadow = true;
group.add(pillar);
});
// 顶部横梁
const topBar = new THREE.Mesh(new THREE.BoxGeometry(4, 0.3, 0.3), redMaterial);
topBar.position.set(0, 5, -1.8);
topBar.castShadow = true;
group.add(topBar);
// 高透风/轻量化面板
const glassPanel = new THREE.Mesh(new THREE.PlaneGeometry(3, 4), glassMaterial);
glassPanel.position.set(0, 2.5, -1.7);
group.add(glassPanel);
// 旗舰款添加红色聚光灯效果
const spotLight = new THREE.SpotLight(0xe11d48, 2);
spotLight.position.set(0, 10, 0);
spotLight.target = group;
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.5;
scene.add(spotLight);
} else if (data.type === 'treadmill') {
// 跑步机
const deck = new THREE.Mesh(new THREE.BoxGeometry(2, 0.3, 4), darkMetal);
deck.position.set(0, 1, 0);
deck.castShadow = true; deck.receiveShadow = true;
group.add(deck);
// 扶手
const handrail = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.2, 0.2), lightMetal);
handrail.position.set(0, 3, -1.8);
handrail.castShadow = true;
group.add(handrail);
// 屏幕
const screen = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1, 0.1), glassMaterial);
screen.position.set(0, 3.5, -1.7);
screen.rotation.x = -0.2;
group.add(screen);
} else if (data.type === 'rower') {
// 划船机
const rail = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.2, 5), lightMetal);
rail.position.set(0, 0.5, 0);
rail.castShadow = true;
group.add(rail);
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.2, 0.8), darkMetal);
seat.position.set(0, 0.7, 0.5);
seat.castShadow = true;
group.add(seat);
} else if (data.type === 'dumbbell') {
// 哑铃架
const frame = new THREE.Mesh(new THREE.BoxGeometry(3, 1.5, 1), redMaterial);
frame.position.set(0, 0.75, 0);
frame.castShadow = true;
group.add(frame);
}
scene.add(group);
equipmentMeshes.push(group);
}
// 初始化生成所有器材
equipmentData.forEach((data, index) => createEquipment(data, index));
// --- 5. 交互逻辑:滑块控制 ---
const slider = document.getElementById('paramSlider');
const sliderValueText = document.getElementById('sliderValue');
const sliderLabel = document.getElementById('sliderLabel');
// 相机目标位置与视点
let targetCameraPos = new THREE.Vector3(0, 15, 35);
let targetLookAt = new THREE.Vector3(0, 0, 0);
let currentFocusIndex = -1;
// 滑块事件监听
slider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
sliderValueText.innerText = val;
// 切换评估维度 (左侧阻力,右侧智能互联)
if (val < 50) {
sliderLabel.innerText = "阻力级别 (Resistance)";
} else {
sliderLabel.innerText = "智能互联度 (Smart Connect)";
}
// 寻找最匹配的器材
let bestMatchIndex = 0;
let minDiff = 999;
const currentParam = val;
equipmentMeshes.forEach((mesh, index) => {
const data = mesh.userData;
// 动态判断是看阻力还是智能互联
const targetValue = currentParam < 50 ? data.res : data.int;
const diff = Math.abs(targetValue - currentParam);
if (diff < minDiff) {
minDiff = diff;
bestMatchIndex = index;
}
});
// 如果目标改变了,触发相机漫游
if (currentFocusIndex !== bestMatchIndex) {
currentFocusIndex = bestMatchIndex;
focusOnEquipment(bestMatchIndex);
}
});
function focusOnEquipment(index) {
const targetMesh = equipmentMeshes[index];
const data = targetMesh.userData;
// 计算相机目标位置 (器材侧前方)
// 保留 Y 轴高度,X 和 Z 根据器材位置偏移
targetCameraPos.set(
data.x * 1.5 + 5,
8,
data.z * 1.5 + 12
);
// 视点对准器材中心
targetLookAt.set(data.x, 1.5, data.z);
// 高亮当前选中的器材 (通过放大或改变材质颜色)
equipmentMeshes.forEach((mesh, i) => {
if (i === index) {
mesh.scale.set(1.1, 1.1, 1.1); // 放大高亮
} else {
mesh.scale.set(1, 1, 1);
}
});
}
// --- 6. 动画循环 ---
function animate() {
requestAnimationFrame(animate);
// 相机平滑过渡 (Lerp)
camera.position.lerp(targetCameraPos, 0.05);
controls.target.lerp(targetLookAt, 0.05);
controls.update();
renderer.render(scene, camera);
}
animate();
// 初始化默认聚焦中央旗舰款
focusOnEquipment(1);
// --- 7. 窗口自适应 ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 简单的悬浮提示 (将 3D 坐标转换为 2D 屏幕坐标)
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
document.body.appendChild(tooltip);
function updateTooltips() {
if (currentFocusIndex >= 0) {
const targetMesh = equipmentMeshes[currentFocusIndex];
const vector = new THREE.Vector3();
targetMesh.getWorldPosition(vector);
vector.y += 3; // 向上偏移
vector.project(camera);
const x = (vector.x * .5 + .5) * window.innerWidth;
const y = (-(vector.y * .5) + .5) * window.innerHeight;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
tooltip.innerText = targetMesh.userData.name;
tooltip.classList.add('active');
} else {
tooltip.classList.remove('active');
}
}
// 在渲染循环中更新 Tooltip 位置
function renderLoop() {
requestAnimationFrame(renderLoop);
updateTooltips();
renderer.render(scene, camera);
}
renderLoop();
</script>
</body>
</html>