docs: module catalog, 3D model, touchscreen concept, and PCB preview
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pi Multi-FX Pedal — 3D Model</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: #0d0d0d;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
#info {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
background: rgba(13,13,13,0.7);
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #333;
|
||||
font-size: 0.8rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
#info span { color: #666; }
|
||||
#key {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
background: rgba(13,13,13,0.8);
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 0.7rem;
|
||||
backdrop-filter: blur(4px);
|
||||
min-width: 120px;
|
||||
}
|
||||
#key div { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
|
||||
.swatch { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }
|
||||
.swatch.lcd { background: #2a3a3a; }
|
||||
.swatch.glass { background: rgba(79,195,247,0.3); }
|
||||
.swatch.disp { background: #1a5a0a; }
|
||||
.swatch.rpi { background: #66bb6a; }
|
||||
.swatch.hat { background: #a78bfa; }
|
||||
.swatch.box { background: #888; }
|
||||
.swatch.jack { background: #fbbf24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="key">
|
||||
<div><span class="swatch glass"></span> Touch Glass</div>
|
||||
<div><span class="swatch disp"></span> Display PCB</div>
|
||||
<div><span class="swatch rpi"></span> Raspberry Pi 4B</div>
|
||||
<div><span class="swatch hat"></span> I2S HAT</div>
|
||||
<div><span class="swatch box"></span> 1590BB Enclosure</div>
|
||||
<div><span class="swatch jack"></span> Audio Jacks</div>
|
||||
<div style="margin-top:6px; color:#888;">🖱 Drag to orbit</div>
|
||||
<div style="color:#888;">🔄 Scroll to zoom</div>
|
||||
</div>
|
||||
|
||||
<div id="info">🎸 Pi Multi-FX Pedal · <span>1590BB + 5" DSI Touch + I2S HAT</span></div>
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
|
||||
|
||||
// ── Scene Setup ──────────────────────────────────────────────────
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x0d0d0d);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 500);
|
||||
camera.position.set(150, 100, 200);
|
||||
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);
|
||||
|
||||
const labelRenderer = new CSS2DRenderer();
|
||||
labelRenderer.setSize(window.innerWidth, window.innerHeight);
|
||||
labelRenderer.domElement.style.position = 'absolute';
|
||||
labelRenderer.domElement.style.top = '0';
|
||||
labelRenderer.domElement.style.left = '0';
|
||||
labelRenderer.domElement.style.pointerEvents = 'none';
|
||||
document.body.appendChild(labelRenderer.domElement);
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.08;
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 1.0;
|
||||
controls.target.set(0, 5, 0);
|
||||
|
||||
// ── Lighting ─────────────────────────────────────────────────────
|
||||
const ambient = new THREE.AmbientLight(0x404060, 0.6);
|
||||
scene.add(ambient);
|
||||
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 2.5);
|
||||
keyLight.position.set(100, 150, 80);
|
||||
keyLight.castShadow = true;
|
||||
keyLight.shadow.mapSize.width = 1024;
|
||||
keyLight.shadow.mapSize.height = 1024;
|
||||
scene.add(keyLight);
|
||||
|
||||
const fillLight = new THREE.DirectionalLight(0x8888ff, 0.6);
|
||||
fillLight.position.set(-80, 40, -60);
|
||||
scene.add(fillLight);
|
||||
|
||||
const rimLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
rimLight.position.set(-50, 100, -100);
|
||||
scene.add(rimLight);
|
||||
|
||||
const underLight = new THREE.DirectionalLight(0x4488ff, 0.25);
|
||||
underLight.position.set(0, -100, 0);
|
||||
scene.add(underLight);
|
||||
|
||||
// Ground grid
|
||||
const gridHelper = new THREE.GridHelper(300, 40, 0x333, 0x222);
|
||||
gridHelper.position.y = -15;
|
||||
scene.add(gridHelper);
|
||||
|
||||
// ── Helper: make a box with rounded edges ────────────────────────
|
||||
function roundedBox(w, h, d, r, color, emissive, roughness, metalness, opacity) {
|
||||
const shape = new THREE.Shape();
|
||||
const w2 = w/2 - r;
|
||||
const h2 = h/2 - r;
|
||||
shape.moveTo(-w2, h2);
|
||||
shape.quadraticCurveTo(w/2, h2, w/2, 0);
|
||||
shape.quadraticCurveTo(w/2, -h2, w2, -h2);
|
||||
shape.lineTo(-w2, -h2);
|
||||
shape.quadraticCurveTo(-w/2, -h2, -w/2, 0);
|
||||
shape.quadraticCurveTo(-w/2, h2, -w2, h2);
|
||||
|
||||
const extrudeSettings = { depth: d, bevelEnabled: true, bevelThickness: r*0.5, bevelSize: r*0.3, bevelSegments: 6 };
|
||||
const geo = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
geo.translate(0, 0, -d/2);
|
||||
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color, emissive, roughness, metalness,
|
||||
transparent: opacity !== undefined,
|
||||
opacity: opacity || 1.0,
|
||||
emissiveIntensity: 0.1,
|
||||
});
|
||||
const mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
return mesh;
|
||||
}
|
||||
|
||||
function makeLabel(text, color = '#4fc3f7', size = '14px') {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
div.style.color = color;
|
||||
div.style.fontFamily = 'Courier New, monospace';
|
||||
div.style.fontSize = size;
|
||||
div.style.fontWeight = '600';
|
||||
div.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)';
|
||||
div.style.background = 'rgba(0,0,0,0.5)';
|
||||
div.style.padding = '2px 8px';
|
||||
div.style.borderRadius = '4px';
|
||||
div.style.border = '1px solid ' + color;
|
||||
return new CSS2DObject(div);
|
||||
}
|
||||
|
||||
// ── Colors ───────────────────────────────────────────────────────
|
||||
const C = {
|
||||
glass: 0x88ccdd,
|
||||
glassEdge: 0x446688,
|
||||
pcb: 0x1a5a0a,
|
||||
rpi: 0x66bb6a,
|
||||
rpiDark: 0x338833,
|
||||
hat: 0x8855cc,
|
||||
hatDark: 0x553388,
|
||||
box: 0x888888,
|
||||
boxDark: 0x555555,
|
||||
jack: 0xfbbf24,
|
||||
jackDark: 0xcc9900,
|
||||
accent: 0x4fc3f7,
|
||||
gold: 0xccaaff,
|
||||
};
|
||||
|
||||
// ── Build the pedal ──────────────────────────────────────────────
|
||||
const pedal = new THREE.Group();
|
||||
|
||||
// 1. ENCLOSURE — 1590BB (140×78×48mm external)
|
||||
// Bottom box
|
||||
const boxGeo = new THREE.BoxGeometry(140, 48, 78);
|
||||
const boxMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x666666,
|
||||
roughness: 0.7,
|
||||
metalness: 0.3,
|
||||
emissive: 0x222222,
|
||||
emissiveIntensity: 0.05,
|
||||
});
|
||||
const boxMesh = new THREE.Mesh(boxGeo, boxMat);
|
||||
boxMesh.position.y = -24;
|
||||
boxMesh.castShadow = true;
|
||||
boxMesh.receiveShadow = true;
|
||||
pedal.add(boxMesh);
|
||||
|
||||
// Top face (panel) — slightly recessed
|
||||
const panelMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x555555,
|
||||
roughness: 0.6,
|
||||
metalness: 0.4,
|
||||
emissive: 0x111111,
|
||||
emissiveIntensity: 0.05,
|
||||
});
|
||||
const panelGeo = new THREE.BoxGeometry(136, 2, 74);
|
||||
const panelMesh = new THREE.Mesh(panelGeo, panelMat);
|
||||
panelMesh.position.y = 0;
|
||||
panelMesh.castShadow = true;
|
||||
panelMesh.receiveShadow = true;
|
||||
pedal.add(panelMesh);
|
||||
|
||||
// Enclosure edge bevels (visual)
|
||||
const edgeMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x444444,
|
||||
roughness: 0.8,
|
||||
metalness: 0.2,
|
||||
});
|
||||
|
||||
// 2. I2S HAT (bottom layer) — 65×56mm, sits inside enclosure
|
||||
const hatMat = new THREE.MeshStandardMaterial({
|
||||
color: C.hat,
|
||||
roughness: 0.4,
|
||||
metalness: 0.3,
|
||||
emissive: C.hat,
|
||||
emissiveIntensity: 0.05,
|
||||
});
|
||||
const hatGeo = new THREE.BoxGeometry(65, 1.6, 56);
|
||||
const hatMesh = new THREE.Mesh(hatGeo, hatMat);
|
||||
hatMesh.position.set(0, 2.5, 0);
|
||||
hatMesh.castShadow = true;
|
||||
hatMesh.receiveShadow = true;
|
||||
pedal.add(hatMesh);
|
||||
|
||||
// HAT silkscreen marks
|
||||
const hatchf = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.5, 0.15, 0.5),
|
||||
new THREE.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xffaa00, emissiveIntensity: 0.3 })
|
||||
);
|
||||
[-20, -10, 0, 10, 20].forEach(x => {
|
||||
[-15, 0, 15].forEach(z => {
|
||||
const dot = hatchf.clone();
|
||||
dot.position.set(x, 3.3, z);
|
||||
pedal.add(dot);
|
||||
});
|
||||
});
|
||||
|
||||
// HAT components (IC blocks)
|
||||
function addIC(x, z, w, d, h, color, label) {
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color, roughness: 0.3, metalness: 0.5,
|
||||
emissive: color, emissiveIntensity: 0.08,
|
||||
});
|
||||
const body = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
|
||||
body.position.set(x, 3.3 + h/2, z);
|
||||
body.castShadow = true;
|
||||
pedal.add(body);
|
||||
|
||||
const lbl = makeLabel(label, '#a78bfa', '10px');
|
||||
lbl.position.set(x, 6, z);
|
||||
pedal.add(lbl);
|
||||
}
|
||||
|
||||
addIC(-15, -8, 8, 6, 1.5, 0xaa66ee, 'U3 ADC');
|
||||
addIC(15, -8, 8, 6, 1.5, 0x66aaff, 'U4 DAC');
|
||||
addIC(-15, 8, 10, 10, 2.0, 0x66bb66, 'U2 Pre');
|
||||
addIC(10, 8, 7, 6, 1.8, 0xee5555, 'U1 LDO');
|
||||
|
||||
// Jacks on I2S HAT — side mounted cylinders
|
||||
function addJack(x, z, angle, label) {
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: C.jack,
|
||||
roughness: 0.3,
|
||||
metalness: 0.6,
|
||||
emissive: C.jack,
|
||||
emissiveIntensity: 0.05,
|
||||
});
|
||||
const cylinder = new THREE.Mesh(new THREE.CylinderGeometry(4, 4, 12, 16), mat);
|
||||
cylinder.rotation.z = Math.PI/2;
|
||||
cylinder.position.set(x, 5, z);
|
||||
cylinder.castShadow = true;
|
||||
pedal.add(cylinder);
|
||||
|
||||
// Nut
|
||||
const nutMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.4, metalness: 0.7 });
|
||||
const nut = new THREE.Mesh(new THREE.CylinderGeometry(5, 5, 2, 6), nutMat);
|
||||
nut.rotation.z = Math.PI/2;
|
||||
nut.position.set(x, 5, z);
|
||||
pedal.add(nut);
|
||||
|
||||
const lbl = makeLabel(label, '#fbbf24', '10px');
|
||||
lbl.position.set(x + (x < 0 ? -10 : 10), 8, z);
|
||||
pedal.add(lbl);
|
||||
}
|
||||
|
||||
addJack(-68, -10, 0, 'J IN');
|
||||
addJack(-68, 10, 0, 'J SEND');
|
||||
addJack(68, -10, 0, 'J RETURN');
|
||||
addJack(68, 10, 0, 'J OUT');
|
||||
|
||||
// 3. RPi 4B (middle layer) — 85×56mm
|
||||
const rpiMat = new THREE.MeshStandardMaterial({
|
||||
color: C.rpi,
|
||||
roughness: 0.5,
|
||||
metalness: 0.2,
|
||||
emissive: C.rpi,
|
||||
emissiveIntensity: 0.03,
|
||||
});
|
||||
const rpiGeo = new THREE.BoxGeometry(85, 1.6, 56);
|
||||
const rpiMesh = new THREE.Mesh(rpiGeo, rpiMat);
|
||||
rpiMesh.position.set(0, 7, 0);
|
||||
rpiMesh.castShadow = true;
|
||||
rpiMesh.receiveShadow = true;
|
||||
pedal.add(rpiMesh);
|
||||
|
||||
// RPi standoffs (4x)
|
||||
const standoffMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x888888, roughness: 0.3, metalness: 0.6,
|
||||
});
|
||||
for (let x of [-28, 28]) {
|
||||
for (let z of [-20, 20]) {
|
||||
const standoff = new THREE.Mesh(new THREE.CylinderGeometry(2.5, 2.5, 5, 8), standoffMat);
|
||||
standoff.position.set(x, 5.5, z);
|
||||
pedal.add(standoff);
|
||||
}
|
||||
}
|
||||
|
||||
// GPIO stacking header
|
||||
const hdrMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x222222, roughness: 0.3, metalness: 0.2,
|
||||
});
|
||||
const hdr = new THREE.Mesh(new THREE.BoxGeometry(4, 6, 50), hdrMat);
|
||||
hdr.position.set(22, 10, 0);
|
||||
pedal.add(hdr);
|
||||
const hdr2 = new THREE.Mesh(new THREE.BoxGeometry(4, 6, 50), hdrMat);
|
||||
hdr2.position.set(-22, 10, 0);
|
||||
pedal.add(hdr2);
|
||||
|
||||
// DSI port indicator
|
||||
const dsiMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x4488ff, roughness: 0.3, metalness: 0.1,
|
||||
});
|
||||
const dsiPort = new THREE.Mesh(new THREE.BoxGeometry(4, 1, 10), dsiMat);
|
||||
dsiPort.position.set(-38, 8, 5);
|
||||
pedal.add(dsiPort);
|
||||
|
||||
const rpiLbl = makeLabel('RPi 4B', '#66bb6a', '12px');
|
||||
rpiLbl.position.set(0, 12, 0);
|
||||
pedal.add(rpiLbl);
|
||||
|
||||
// 4. Display PCB (Waveshare 5" — 102×76mm)
|
||||
const dispMat = new THREE.MeshStandardMaterial({
|
||||
color: C.pcb,
|
||||
roughness: 0.4,
|
||||
metalness: 0.2,
|
||||
emissive: C.pcb,
|
||||
emissiveIntensity: 0.03,
|
||||
});
|
||||
const dispGeo = new THREE.BoxGeometry(102, 1.6, 76);
|
||||
const dispMesh = new THREE.Mesh(dispGeo, dispMat);
|
||||
dispMesh.position.set(0, 13, 0);
|
||||
dispMesh.castShadow = true;
|
||||
dispMesh.receiveShadow = true;
|
||||
pedal.add(dispMesh);
|
||||
|
||||
// Display standoffs
|
||||
for (let x of [-40, 40]) {
|
||||
for (let z of [-30, 30]) {
|
||||
const standoff = new THREE.Mesh(new THREE.CylinderGeometry(2, 2, 5, 8), standoffMat);
|
||||
standoff.position.set(x, 11, z);
|
||||
pedal.add(standoff);
|
||||
}
|
||||
}
|
||||
|
||||
// DSI ribbon cable (visual)
|
||||
const ribbonMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x8855cc, roughness: 0.5, metalness: 0.1,
|
||||
transparent: true, opacity: 0.7,
|
||||
});
|
||||
const ribbon = new THREE.Mesh(new THREE.BoxGeometry(3, 1, 40), ribbonMat);
|
||||
ribbon.position.set(-40, 12.5, -20);
|
||||
ribbon.rotation.x = 0.3;
|
||||
pedal.add(ribbon);
|
||||
const ribbon2 = new THREE.Mesh(new THREE.BoxGeometry(3, 1, 30), ribbonMat);
|
||||
ribbon2.position.set(-40, 10, -35);
|
||||
ribbon2.rotation.x = 0.6;
|
||||
pedal.add(ribbon2);
|
||||
|
||||
const dispLbl = makeLabel('5" DSI Display', '#33aa33', '12px');
|
||||
dispLbl.position.set(0, 16, 0);
|
||||
pedal.add(dispLbl);
|
||||
|
||||
// 5. Touch Glass (front face — fills enclosure top)
|
||||
const glassMat = new THREE.MeshStandardMaterial({
|
||||
color: C.glass,
|
||||
roughness: 0.1,
|
||||
metalness: 0.0,
|
||||
transparent: true,
|
||||
opacity: 0.15,
|
||||
emissive: C.glass,
|
||||
emissiveIntensity: 0.05,
|
||||
});
|
||||
const glassGeo = new THREE.BoxGeometry(120, 1.5, 65);
|
||||
const glassMesh = new THREE.Mesh(glassGeo, glassMat);
|
||||
glassMesh.position.set(0, 15.5, 0);
|
||||
glassMesh.castShadow = true;
|
||||
glassMesh.receiveShadow = true;
|
||||
pedal.add(glassMesh);
|
||||
|
||||
// Glass edge bezel
|
||||
const bezelMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x222233, roughness: 0.4, metalness: 0.3,
|
||||
});
|
||||
const bezel = new THREE.Mesh(new THREE.BoxGeometry(122, 0.5, 67), bezelMat);
|
||||
bezel.position.set(0, 15, 0);
|
||||
pedal.add(bezel);
|
||||
|
||||
// Simulated UI on glass — floating colored rectangles
|
||||
function addUIPixel(x, z, w, h, color) {
|
||||
const m = new THREE.MeshStandardMaterial({
|
||||
color, roughness: 0.3, metalness: 0.0,
|
||||
emissive: color, emissiveIntensity: 0.3,
|
||||
transparent: true, opacity: 0.6,
|
||||
});
|
||||
const r = new THREE.Mesh(new THREE.BoxGeometry(w, 0.1, h), m);
|
||||
r.position.set(x, 16.3, z);
|
||||
pedal.add(r);
|
||||
}
|
||||
|
||||
// Simulated preset name bar
|
||||
addUIPixel(-30, 18, 60, 8, 0x4fc3f7);
|
||||
// Bypass button glow
|
||||
addUIPixel(-45, -5, 20, 10, 0x66bb6a);
|
||||
// Volume slider
|
||||
addUIPixel(5, -5, 40, 4, 0x4fc3f7);
|
||||
// Volume knob
|
||||
addUIPixel(30, -5, 6, 6, 0x4fc3f7);
|
||||
// FX chain pills
|
||||
addUIPixel(-30, -18, 25, 6, 0xa78bfa);
|
||||
addUIPixel(-5, -18, 18, 6, 0x66bb6a);
|
||||
addUIPixel(15, -18, 22, 6, 0x4fc3f7);
|
||||
// Bottom bar
|
||||
addUIPixel(-30, -28, 70, 6, 0x4488cc);
|
||||
|
||||
const glassLbl = makeLabel('Touch Glass', '#88ccdd', '12px');
|
||||
glassLbl.position.set(60, 20, -30);
|
||||
pedal.add(glassLbl);
|
||||
|
||||
// ── Position the whole pedal ───────────────────────────────────
|
||||
pedal.position.set(0, 10, 0);
|
||||
scene.add(pedal);
|
||||
|
||||
// ── Label: overall product name ────────────────────────────────
|
||||
const titleLbl = makeLabel('Pi Multi-FX Pedal · 1590BB', '#4fc3f7', '18px');
|
||||
titleLbl.position.set(0, 45, 80);
|
||||
scene.add(titleLbl);
|
||||
|
||||
const subtitleLbl = makeLabel('5" DSI Touch · PCM1808+PCM5102 I2S HAT · 32 FX', '#888', '12px');
|
||||
subtitleLbl.position.set(0, 40, 80);
|
||||
scene.add(subtitleLbl);
|
||||
|
||||
// ── Responsive ──────────────────────────────────────────────────
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
labelRenderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
// Toggle auto-rotate on click
|
||||
renderer.domElement.addEventListener('click', () => {
|
||||
controls.autoRotate = !controls.autoRotate;
|
||||
});
|
||||
|
||||
// ── Animate ──────────────────────────────────────────────────────
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
labelRenderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user