Files

510 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>