generated from CubeCraft-Creations/Tracehound
feat: add interactive 3D case viewer (Three.js)
Rotatable 3D render of the tripod-mounted dual-ESP case: - Case body with rounded corners and lid - Stacked ESP32 + ESP8266 boards inside - LED indicator, USB port, ventilation slots - Tripod pole with C-clamp mount - USB cables, screws, chip details - Drag to rotate, scroll to zoom - Open in any browser
This commit is contained in:
@@ -0,0 +1,274 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>RemoteRig Case — 3D Viewer</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; overflow: hidden; background: #1a1a2e; font-family: system-ui; }
|
||||||
|
canvas { display: block; }
|
||||||
|
#info {
|
||||||
|
position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
|
||||||
|
color: #888; font-size: 13px; pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// ── Scene setup ──
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x1a1a2e);
|
||||||
|
scene.fog = new THREE.Fog(0x1a1a2e, 8, 25);
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.5, 50);
|
||||||
|
camera.position.set(5, 3.5, 7);
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 1.2;
|
||||||
|
document.body.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// ── Lighting ──
|
||||||
|
const ambient = new THREE.AmbientLight(0x404060, 0.6);
|
||||||
|
scene.add(ambient);
|
||||||
|
const key = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||||
|
key.position.set(8, 10, 5);
|
||||||
|
key.castShadow = true;
|
||||||
|
key.shadow.mapSize.set(2048, 2048);
|
||||||
|
key.shadow.camera.near = 0.5; key.shadow.camera.far = 50;
|
||||||
|
key.shadow.camera.left = -10; key.shadow.camera.right = 10;
|
||||||
|
key.shadow.camera.top = 10; key.shadow.camera.bottom = -10;
|
||||||
|
scene.add(key);
|
||||||
|
const fill = new THREE.DirectionalLight(0x8899cc, 0.4);
|
||||||
|
fill.position.set(-3, 2, -2);
|
||||||
|
scene.add(fill);
|
||||||
|
const rim = new THREE.DirectionalLight(0xaaccff, 0.5);
|
||||||
|
rim.position.set(0, 1, -5);
|
||||||
|
scene.add(rim);
|
||||||
|
|
||||||
|
// ── Ground ──
|
||||||
|
const ground = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(20, 20),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x2a2a3e, roughness: 0.8 })
|
||||||
|
);
|
||||||
|
ground.rotation.x = -Math.PI/2;
|
||||||
|
ground.position.y = -3;
|
||||||
|
ground.receiveShadow = true;
|
||||||
|
scene.add(ground);
|
||||||
|
|
||||||
|
// ── Materials ──
|
||||||
|
const petgMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x3d3d4a, roughness: 0.35, metalness: 0.1,
|
||||||
|
});
|
||||||
|
const accentMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xf59e0b, roughness: 0.3, metalness: 0.2, emissive: 0x331100, emissiveIntensity: 0.3
|
||||||
|
});
|
||||||
|
const boardMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x1a6630, roughness: 0.6
|
||||||
|
});
|
||||||
|
const metalMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x888899, roughness: 0.3, metalness: 0.8
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Create rounded box with bevel ──
|
||||||
|
function createRoundedBox(w, h, d, r, segments = 3) {
|
||||||
|
const shape = new THREE.Shape();
|
||||||
|
const hw = w/2 - r, hh = h/2 - r;
|
||||||
|
shape.moveTo(-hw, -hh + r);
|
||||||
|
shape.quadraticCurveTo(-hw, -hh, -hw + r, -hh);
|
||||||
|
shape.lineTo(hw - r, -hh);
|
||||||
|
shape.quadraticCurveTo(hw, -hh, hw, -hh + r);
|
||||||
|
shape.lineTo(hw, hh - r);
|
||||||
|
shape.quadraticCurveTo(hw, hh, hw - r, hh);
|
||||||
|
shape.lineTo(-hw + r, hh);
|
||||||
|
shape.quadraticCurveTo(-hw, hh, -hw, hh - r);
|
||||||
|
shape.closePath();
|
||||||
|
|
||||||
|
const extrudeSettings = { depth: d - r*2, bevelEnabled: true, bevelThickness: r, bevelSize: r, bevelSegments: segments };
|
||||||
|
const geom = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||||
|
geom.translate(0, 0, -d/2 + r);
|
||||||
|
return geom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Case Body ──
|
||||||
|
const caseW = 2.5, caseH = 1.5, caseD = 1.1;
|
||||||
|
const bodyGeom = createRoundedBox(caseW, caseD, caseH, 0.12);
|
||||||
|
const body = new THREE.Mesh(bodyGeom, petgMat);
|
||||||
|
body.castShadow = true; body.receiveShadow = true;
|
||||||
|
scene.add(body);
|
||||||
|
|
||||||
|
// ── Lid (slightly offset) ──
|
||||||
|
const lidGeom = createRoundedBox(caseW, caseD, 0.15, 0.08);
|
||||||
|
const lid = new THREE.Mesh(lidGeom, petgMat);
|
||||||
|
lid.position.y = caseH/2 + 0.07;
|
||||||
|
lid.castShadow = true;
|
||||||
|
scene.add(lid);
|
||||||
|
|
||||||
|
// ── Ventilation slots ──
|
||||||
|
for (let i = -0.6; i <= 0.6; i += 0.6) {
|
||||||
|
const slot = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.4, 0.04, caseD * 0.7),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x1a1a2e })
|
||||||
|
);
|
||||||
|
slot.position.set(i, caseH/2 + 0.15, 0);
|
||||||
|
scene.add(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Screws ──
|
||||||
|
for (let x = -1; x <= 1; x += 2) {
|
||||||
|
for (let z = -0.35; z <= 0.35; z += 0.7) {
|
||||||
|
const screw = new THREE.Mesh(
|
||||||
|
new THREE.CylinderGeometry(0.05, 0.05, 0.04, 8),
|
||||||
|
metalMat
|
||||||
|
);
|
||||||
|
screw.position.set(x * (caseW/2 - 0.2), caseH/2 + 0.15, z);
|
||||||
|
scene.add(screw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Boards inside (semi-visible) ──
|
||||||
|
const esp32Board = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(caseW - 0.3, 0.04, caseD - 0.2),
|
||||||
|
boardMat
|
||||||
|
);
|
||||||
|
esp32Board.position.set(0, caseH/2 - 0.15, 0);
|
||||||
|
esp32Board.castShadow = true;
|
||||||
|
scene.add(esp32Board);
|
||||||
|
|
||||||
|
const esp8266Board = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(caseW - 0.5, 0.04, caseD - 0.3),
|
||||||
|
boardMat
|
||||||
|
);
|
||||||
|
esp8266Board.position.set(0, caseH/2 - 0.08, 0);
|
||||||
|
esp8266Board.castShadow = true;
|
||||||
|
scene.add(esp8266Board);
|
||||||
|
|
||||||
|
// Chip on ESP32
|
||||||
|
const chip = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.3, 0.03, 0.3),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x111122, roughness: 0.2 })
|
||||||
|
);
|
||||||
|
chip.position.set(0, caseH/2 - 0.12, 0);
|
||||||
|
scene.add(chip);
|
||||||
|
|
||||||
|
// LED
|
||||||
|
const led = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.03, 8, 8),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x00ff44, roughness: 0.2, emissive: 0x00ff44, emissiveIntensity: 1.5 })
|
||||||
|
);
|
||||||
|
led.position.set(-0.8, caseH/2 - 0.12, -0.3);
|
||||||
|
scene.add(led);
|
||||||
|
|
||||||
|
// ── USB Port (front face) ──
|
||||||
|
const usbPort = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.35, 0.02, 0.15),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x111122, roughness: 0.2 })
|
||||||
|
);
|
||||||
|
usbPort.position.set(0, 0.2, caseD/2);
|
||||||
|
scene.add(usbPort);
|
||||||
|
|
||||||
|
// ── Tripod Clip ──
|
||||||
|
const clipGroup = new THREE.Group();
|
||||||
|
clipGroup.position.set(0, 0, -caseD/2 - 0.7);
|
||||||
|
|
||||||
|
// Clip arms
|
||||||
|
for (let y = -0.4; y <= 0.4; y += 0.8) {
|
||||||
|
const arm = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.4, 0.08, 0.8),
|
||||||
|
petgMat
|
||||||
|
);
|
||||||
|
arm.position.set(0, y, 0.3);
|
||||||
|
arm.castShadow = true;
|
||||||
|
clipGroup.add(arm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip body
|
||||||
|
const clipBody = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(0.4, 1.0, 0.15),
|
||||||
|
petgMat
|
||||||
|
);
|
||||||
|
clipBody.position.set(0, 0, -0.1);
|
||||||
|
clipBody.castShadow = true;
|
||||||
|
clipGroup.add(clipBody);
|
||||||
|
|
||||||
|
scene.add(clipGroup);
|
||||||
|
|
||||||
|
// ── Tripod Pole ──
|
||||||
|
const poleGeom = new THREE.CylinderGeometry(0.35, 0.35, 6, 24);
|
||||||
|
const poleMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.4, metalness: 0.3 });
|
||||||
|
const pole = new THREE.Mesh(poleGeom, poleMat);
|
||||||
|
pole.position.set(0, 0, -caseD/2 - 1.2);
|
||||||
|
pole.castShadow = true; pole.receiveShadow = true;
|
||||||
|
scene.add(pole);
|
||||||
|
|
||||||
|
// ── USB Cables ──
|
||||||
|
function createCable(start, end, color = 0x222233) {
|
||||||
|
const curve = new THREE.CubicBezierCurve3(
|
||||||
|
start,
|
||||||
|
new THREE.Vector3(start.x + 0.5, start.y - 0.5, start.z + 0.2),
|
||||||
|
new THREE.Vector3(end.x - 0.3, end.y - 0.3, end.z + 0.1),
|
||||||
|
end
|
||||||
|
);
|
||||||
|
const geom = new THREE.TubeGeometry(curve, 20, 0.03, 8, false);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.6 });
|
||||||
|
return new THREE.Mesh(geom, mat);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cable1 = createCable(
|
||||||
|
new THREE.Vector3(0, 0.2, caseD/2),
|
||||||
|
new THREE.Vector3(-2, -1, 1)
|
||||||
|
);
|
||||||
|
cable1.castShadow = true;
|
||||||
|
scene.add(cable1);
|
||||||
|
|
||||||
|
const cable2 = createCable(
|
||||||
|
new THREE.Vector3(0.1, 0.2, caseD/2),
|
||||||
|
new THREE.Vector3(2, -1.5, 1.2),
|
||||||
|
0x332222
|
||||||
|
);
|
||||||
|
cable2.castShadow = true;
|
||||||
|
scene.add(cable2);
|
||||||
|
|
||||||
|
// ── Interaction ──
|
||||||
|
let isDragging = false, prevMouse = { x: 0, y: 0 };
|
||||||
|
let rotY = 0.4, rotX = 0.3, zoom = 7;
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', e => { isDragging = true; prevMouse = { x: e.clientX, y: e.clientY }; });
|
||||||
|
document.addEventListener('mouseup', () => isDragging = false);
|
||||||
|
document.addEventListener('mousemove', e => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
rotY += (e.clientX - prevMouse.x) * 0.005;
|
||||||
|
rotX += (e.clientY - prevMouse.y) * 0.005;
|
||||||
|
rotX = Math.max(-0.8, Math.min(1.2, rotX));
|
||||||
|
prevMouse = { x: e.clientX, y: e.clientY };
|
||||||
|
});
|
||||||
|
document.addEventListener('wheel', e => {
|
||||||
|
zoom += e.deltaY * 0.005;
|
||||||
|
zoom = Math.max(3, Math.min(15, zoom));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Render loop ──
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
camera.position.x = zoom * Math.sin(rotY) * Math.cos(rotX);
|
||||||
|
camera.position.y = zoom * Math.sin(rotX);
|
||||||
|
camera.position.z = zoom * Math.cos(rotY) * Math.cos(rotX);
|
||||||
|
camera.lookAt(0, -0.1, 0);
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user