510 lines
16 KiB
HTML
510 lines
16 KiB
HTML
<!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> |