Knowledge base of ~80+ markdown files across 14 domains (00-13), Logseq graph, hardware design files (KiCAD), infrastructure configs, and talas-wiki static site. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
987 lines
28 KiB
HTML
987 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>RĒVO — Le Micro Réparable</title>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,700;1,9..40,300&family=Space+Mono:wght@400;700&display=swap');
|
|
|
|
:root {
|
|
--bg: #0a0a0a;
|
|
--bg-subtle: #111111;
|
|
--text: #e8e4df;
|
|
--text-muted: #7a756e;
|
|
--accent: #c8956c;
|
|
--accent-bright: #e8b08a;
|
|
--accent-dim: #8a6348;
|
|
--green: #7ab07a;
|
|
--green-dim: #3a5a3a;
|
|
--serif: 'DM Sans', sans-serif;
|
|
--mono: 'Space Mono', monospace;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
html {
|
|
scroll-behavior: auto;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--accent-dim) var(--bg);
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--serif);
|
|
overflow-x: hidden;
|
|
cursor: default;
|
|
}
|
|
|
|
/* ── SCROLL SPACER ── */
|
|
.scroll-spacer {
|
|
height: 600vh;
|
|
position: relative;
|
|
}
|
|
|
|
/* ── 3D CANVAS ── */
|
|
#scene-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100vh;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* ── OVERLAY UI ── */
|
|
.overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100vh;
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ── NAV ── */
|
|
.nav {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
z-index: 100;
|
|
padding: 28px 48px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
mix-blend-mode: difference;
|
|
}
|
|
|
|
.nav-logo {
|
|
font-family: var(--mono);
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
letter-spacing: 6px;
|
|
color: #fff;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.nav-links {
|
|
display: flex;
|
|
gap: 32px;
|
|
list-style: none;
|
|
}
|
|
|
|
.nav-links a {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
letter-spacing: 2px;
|
|
color: #fff;
|
|
text-decoration: none;
|
|
text-transform: uppercase;
|
|
opacity: 0.6;
|
|
transition: opacity 0.3s;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.nav-links a:hover { opacity: 1; }
|
|
|
|
/* ── HERO TEXT ── */
|
|
.hero-text {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 60px;
|
|
transform: translateY(-50%);
|
|
z-index: 20;
|
|
pointer-events: none;
|
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
}
|
|
|
|
.hero-text h1 {
|
|
font-family: var(--serif);
|
|
font-size: clamp(36px, 5vw, 64px);
|
|
font-weight: 300;
|
|
line-height: 1.1;
|
|
letter-spacing: -1px;
|
|
color: var(--text);
|
|
max-width: 400px;
|
|
}
|
|
|
|
.hero-text h1 em {
|
|
font-style: italic;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.hero-text .subtitle {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.scroll-hint {
|
|
position: fixed;
|
|
bottom: 40px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 20;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: opacity 0.5s;
|
|
}
|
|
|
|
.scroll-hint span {
|
|
font-family: var(--mono);
|
|
font-size: 10px;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.scroll-line {
|
|
width: 1px;
|
|
height: 40px;
|
|
background: var(--accent-dim);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.scroll-line::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: -100%;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--accent);
|
|
animation: scrollPulse 1.8s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes scrollPulse {
|
|
0% { top: -100%; }
|
|
50% { top: 100%; }
|
|
100% { top: 100%; }
|
|
}
|
|
|
|
/* ── PART LABELS ── */
|
|
.part-label {
|
|
position: fixed;
|
|
z-index: 20;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
|
transform: translateX(20px);
|
|
}
|
|
|
|
.part-label.visible {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.part-label .tag {
|
|
font-family: var(--mono);
|
|
font-size: 9px;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
margin-bottom: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.part-label .tag::before {
|
|
content: '';
|
|
display: block;
|
|
width: 20px;
|
|
height: 1px;
|
|
background: var(--accent);
|
|
}
|
|
|
|
.part-label .name {
|
|
font-family: var(--serif);
|
|
font-size: clamp(22px, 3vw, 36px);
|
|
font-weight: 300;
|
|
line-height: 1.2;
|
|
color: var(--text);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.part-label .desc {
|
|
font-family: var(--serif);
|
|
font-size: 14px;
|
|
line-height: 1.7;
|
|
color: var(--text-muted);
|
|
max-width: 320px;
|
|
}
|
|
|
|
.part-label .spec {
|
|
margin-top: 14px;
|
|
display: flex;
|
|
gap: 20px;
|
|
}
|
|
|
|
.part-label .spec-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.part-label .spec-val {
|
|
font-family: var(--mono);
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--accent-bright);
|
|
}
|
|
|
|
.part-label .spec-key {
|
|
font-family: var(--mono);
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* ── REPAIRABILITY BADGE ── */
|
|
.repair-badge {
|
|
position: fixed;
|
|
z-index: 20;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
|
|
.repair-badge.visible { opacity: 1; }
|
|
|
|
.repair-badge-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
background: rgba(58, 90, 58, 0.15);
|
|
border: 1px solid var(--green-dim);
|
|
border-radius: 8px;
|
|
padding: 14px 20px;
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
|
|
.repair-score {
|
|
font-family: var(--mono);
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
color: var(--green);
|
|
}
|
|
|
|
.repair-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.repair-info .ri-title {
|
|
font-family: var(--mono);
|
|
font-size: 10px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--green);
|
|
}
|
|
|
|
.repair-info .ri-desc {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* ── PROGRESS BAR ── */
|
|
.progress-track {
|
|
position: fixed;
|
|
right: 32px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
z-index: 50;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.progress-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--text-muted);
|
|
opacity: 0.3;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.progress-dot.active {
|
|
background: var(--accent);
|
|
opacity: 1;
|
|
box-shadow: 0 0 10px rgba(200, 149, 108, 0.4);
|
|
}
|
|
|
|
/* ── BUY SECTION ── */
|
|
.buy-section {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
z-index: 30;
|
|
padding: 0 48px 48px;
|
|
opacity: 0;
|
|
transform: translateY(40px);
|
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
|
pointer-events: none;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.buy-section.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: all;
|
|
}
|
|
|
|
.buy-card {
|
|
background: rgba(17, 17, 17, 0.85);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(200, 149, 108, 0.15);
|
|
border-radius: 16px;
|
|
padding: 32px 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 40px;
|
|
max-width: 700px;
|
|
width: 100%;
|
|
}
|
|
|
|
.buy-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.buy-info h3 {
|
|
font-family: var(--serif);
|
|
font-size: 22px;
|
|
font-weight: 400;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.buy-info .price {
|
|
font-family: var(--mono);
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: var(--accent-bright);
|
|
}
|
|
|
|
.buy-info .price-note {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.buy-btn {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
color: var(--bg);
|
|
background: linear-gradient(135deg, var(--accent), var(--accent-bright));
|
|
border: none;
|
|
border-radius: 60px;
|
|
padding: 18px 40px;
|
|
cursor: pointer;
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.buy-btn:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 8px 32px rgba(200, 149, 108, 0.3);
|
|
}
|
|
|
|
/* ── VIGNETTE ── */
|
|
.vignette {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
z-index: 5;
|
|
pointer-events: none;
|
|
background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.6) 100%);
|
|
}
|
|
|
|
/* ── GRAIN ── */
|
|
.grain {
|
|
position: fixed;
|
|
top: -50%; left: -50%;
|
|
width: 200%; height: 200%;
|
|
z-index: 999;
|
|
pointer-events: none;
|
|
opacity: 0.03;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
background-repeat: repeat;
|
|
background-size: 256px 256px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="grain"></div>
|
|
<div class="vignette"></div>
|
|
|
|
<!-- NAV -->
|
|
<nav class="nav">
|
|
<div class="nav-logo">Rēvo</div>
|
|
<ul class="nav-links">
|
|
<li><a href="#">Manifeste</a></li>
|
|
<li><a href="#">Pièces détachées</a></li>
|
|
<li><a href="#">Communauté</a></li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<!-- 3D SCENE -->
|
|
<div id="scene-container"></div>
|
|
|
|
<!-- HERO TEXT -->
|
|
<div class="hero-text" id="hero-text">
|
|
<h1>Conçu pour<br>être <em>réparé</em></h1>
|
|
<div class="subtitle">Micro condensateur — Fait pour durer</div>
|
|
</div>
|
|
|
|
<!-- SCROLL HINT -->
|
|
<div class="scroll-hint" id="scroll-hint">
|
|
<div class="scroll-line"></div>
|
|
<span>Explorer</span>
|
|
</div>
|
|
|
|
<!-- PART LABELS -->
|
|
<div class="part-label" id="label-grille" style="top: 22%; right: 60px;">
|
|
<div class="tag">01 — Grille</div>
|
|
<div class="name">Grille acoustique</div>
|
|
<div class="desc">Acier inoxydable micro-perforé. Clip magnétique : retrait sans outil en 2 secondes. Lavable, remplaçable à vie.</div>
|
|
<div class="spec">
|
|
<div class="spec-item"><span class="spec-val">304L</span><span class="spec-key">Inox</span></div>
|
|
<div class="spec-item"><span class="spec-val">0.8mm</span><span class="spec-key">Perforations</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="part-label" id="label-capsule" style="top: 30%; left: 60px;">
|
|
<div class="tag">02 — Capsule</div>
|
|
<div class="name">Capsule à condensateur</div>
|
|
<div class="desc">Membrane plaquée or 34mm, directivité cardioïde. Connecteur enfichable standardisé — remplacement en 30 secondes.</div>
|
|
<div class="spec">
|
|
<div class="spec-item"><span class="spec-val">34mm</span><span class="spec-key">Membrane</span></div>
|
|
<div class="spec-item"><span class="spec-val">20Hz-20kHz</span><span class="spec-key">Réponse</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="part-label" id="label-pcb" style="top: 42%; right: 60px;">
|
|
<div class="tag">03 — Électronique</div>
|
|
<div class="name">PCB modulaire</div>
|
|
<div class="desc">Circuit imprimé open-source avec préampli classe A discret. Composants traversants soudables à la main. Schémas publiés sous licence libre.</div>
|
|
<div class="spec">
|
|
<div class="spec-item"><span class="spec-val">-7dB</span><span class="spec-key">Sensibilité</span></div>
|
|
<div class="spec-item"><span class="spec-val">THT</span><span class="spec-key">Composants</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="part-label" id="label-body" style="top: 55%; left: 60px;">
|
|
<div class="tag">04 — Corps</div>
|
|
<div class="name">Fût en aluminium</div>
|
|
<div class="desc">Aluminium 6061-T6 usiné CNC, anodisé noir mat. Assemblage par 4 vis apparentes — aucune colle, aucun clip cassable.</div>
|
|
<div class="spec">
|
|
<div class="spec-item"><span class="spec-val">6061</span><span class="spec-key">Alliage</span></div>
|
|
<div class="spec-item"><span class="spec-val">4 vis</span><span class="spec-key">Assemblage</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="part-label" id="label-connector" style="top: 68%; right: 60px;">
|
|
<div class="tag">05 — Connecteur</div>
|
|
<div class="name">Embase XLR</div>
|
|
<div class="desc">Connecteur Neutrik plaqué or, vissé et non soudé au châssis. Remplacement possible sans fer à souder.</div>
|
|
<div class="spec">
|
|
<div class="spec-item"><span class="spec-val">XLR-3</span><span class="spec-key">Standard</span></div>
|
|
<div class="spec-item"><span class="spec-val">Or</span><span class="spec-key">Contacts</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- REPAIRABILITY BADGE -->
|
|
<div class="repair-badge" id="repair-badge" style="bottom: 120px; left: 60px;">
|
|
<div class="repair-badge-inner">
|
|
<div class="repair-score">10/10</div>
|
|
<div class="repair-info">
|
|
<div class="ri-title">Indice de réparabilité</div>
|
|
<div class="ri-desc">Chaque pièce est remplaçable individuellement</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PROGRESS DOTS -->
|
|
<div class="progress-track" id="progress-track">
|
|
<div class="progress-dot active" data-section="0"></div>
|
|
<div class="progress-dot" data-section="1"></div>
|
|
<div class="progress-dot" data-section="2"></div>
|
|
<div class="progress-dot" data-section="3"></div>
|
|
<div class="progress-dot" data-section="4"></div>
|
|
<div class="progress-dot" data-section="5"></div>
|
|
<div class="progress-dot" data-section="6"></div>
|
|
</div>
|
|
|
|
<!-- BUY SECTION -->
|
|
<div class="buy-section" id="buy-section">
|
|
<div class="buy-card">
|
|
<div class="buy-info">
|
|
<h3>RĒVO Condenser</h3>
|
|
<div class="price">249 €</div>
|
|
<div class="price-note">Garantie 10 ans · Pièces à vie</div>
|
|
</div>
|
|
<button class="buy-btn">Précommander</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SCROLL SPACER -->
|
|
<div class="scroll-spacer"></div>
|
|
|
|
<script>
|
|
// ────────────────────────────────────────────
|
|
// THREE.JS SCENE SETUP
|
|
// ────────────────────────────────────────────
|
|
const container = document.getElementById('scene-container');
|
|
const scene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100);
|
|
camera.position.set(0, 2, 8);
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.1;
|
|
container.appendChild(renderer.domElement);
|
|
|
|
// ── LIGHTS ──
|
|
const ambientLight = new THREE.AmbientLight(0x998877, 0.3);
|
|
scene.add(ambientLight);
|
|
|
|
const keyLight = new THREE.DirectionalLight(0xffeedd, 1.2);
|
|
keyLight.position.set(3, 5, 4);
|
|
scene.add(keyLight);
|
|
|
|
const fillLight = new THREE.DirectionalLight(0x8899bb, 0.4);
|
|
fillLight.position.set(-3, 2, -2);
|
|
scene.add(fillLight);
|
|
|
|
const rimLight = new THREE.DirectionalLight(0xc8956c, 0.6);
|
|
rimLight.position.set(0, -1, -4);
|
|
scene.add(rimLight);
|
|
|
|
const topLight = new THREE.PointLight(0xc8956c, 0.5, 10);
|
|
topLight.position.set(0, 5, 0);
|
|
scene.add(topLight);
|
|
|
|
// ── MATERIALS ──
|
|
const metalDark = new THREE.MeshStandardMaterial({
|
|
color: 0x1a1a1a, metalness: 0.9, roughness: 0.25
|
|
});
|
|
const metalMid = new THREE.MeshStandardMaterial({
|
|
color: 0x2a2a2a, metalness: 0.85, roughness: 0.3
|
|
});
|
|
const metalGrille = new THREE.MeshStandardMaterial({
|
|
color: 0x333333, metalness: 0.95, roughness: 0.2, wireframe: false
|
|
});
|
|
const pcbGreen = new THREE.MeshStandardMaterial({
|
|
color: 0x1a4a2a, metalness: 0.3, roughness: 0.6
|
|
});
|
|
const pcbCopper = new THREE.MeshStandardMaterial({
|
|
color: 0xb87333, metalness: 0.9, roughness: 0.3
|
|
});
|
|
const goldMat = new THREE.MeshStandardMaterial({
|
|
color: 0xc8956c, metalness: 0.95, roughness: 0.15
|
|
});
|
|
const capsuleMat = new THREE.MeshStandardMaterial({
|
|
color: 0x222222, metalness: 0.7, roughness: 0.4
|
|
});
|
|
|
|
// ── MICROPHONE PARTS ──
|
|
const micGroup = new THREE.Group();
|
|
scene.add(micGroup);
|
|
|
|
// Part groups with target offsets
|
|
const parts = {
|
|
grille: { group: new THREE.Group(), baseY: 2.4, explodeY: 4.8 },
|
|
capsule: { group: new THREE.Group(), baseY: 1.6, explodeY: 2.8 },
|
|
pcb: { group: new THREE.Group(), baseY: 0.6, explodeY: 0.6 },
|
|
body: { group: new THREE.Group(), baseY: -0.4, explodeY: -1.6 },
|
|
connector: { group: new THREE.Group(), baseY: -1.8, explodeY: -3.8 },
|
|
};
|
|
|
|
// ── BUILD GRILLE ──
|
|
(() => {
|
|
const g = parts.grille.group;
|
|
// Outer shell
|
|
const sphereGeo = new THREE.SphereGeometry(0.62, 32, 24, 0, Math.PI * 2, 0, Math.PI * 0.55);
|
|
const grilleMesh = new THREE.Mesh(sphereGeo, metalGrille);
|
|
grilleMesh.position.y = 0;
|
|
g.add(grilleMesh);
|
|
// Ring at base of grille
|
|
const ringGeo = new THREE.TorusGeometry(0.6, 0.04, 12, 48);
|
|
const ring = new THREE.Mesh(ringGeo, goldMat);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = -0.15;
|
|
g.add(ring);
|
|
// Inner mesh pattern (wireframe sphere inside)
|
|
const innerGeo = new THREE.SphereGeometry(0.55, 16, 12, 0, Math.PI * 2, 0, Math.PI * 0.5);
|
|
const innerMesh = new THREE.Mesh(innerGeo, new THREE.MeshStandardMaterial({
|
|
color: 0x222222, metalness: 0.8, roughness: 0.3, wireframe: true
|
|
}));
|
|
innerMesh.position.y = 0;
|
|
g.add(innerMesh);
|
|
g.position.y = parts.grille.baseY;
|
|
micGroup.add(g);
|
|
})();
|
|
|
|
// ── BUILD CAPSULE ──
|
|
(() => {
|
|
const g = parts.capsule.group;
|
|
// Housing
|
|
const housingGeo = new THREE.CylinderGeometry(0.5, 0.55, 0.6, 32);
|
|
const housing = new THREE.Mesh(housingGeo, metalMid);
|
|
g.add(housing);
|
|
// Gold ring
|
|
const ringGeo = new THREE.TorusGeometry(0.52, 0.025, 12, 48);
|
|
const ring = new THREE.Mesh(ringGeo, goldMat);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = 0.25;
|
|
g.add(ring);
|
|
// Diaphragm disc
|
|
const diaGeo = new THREE.CylinderGeometry(0.38, 0.38, 0.03, 32);
|
|
const dia = new THREE.Mesh(diaGeo, goldMat);
|
|
dia.position.y = 0.15;
|
|
g.add(dia);
|
|
g.position.y = parts.capsule.baseY;
|
|
micGroup.add(g);
|
|
})();
|
|
|
|
// ── BUILD PCB ──
|
|
(() => {
|
|
const g = parts.pcb.group;
|
|
// Main board
|
|
const boardGeo = new THREE.BoxGeometry(0.8, 0.08, 0.8);
|
|
const board = new THREE.Mesh(boardGeo, pcbGreen);
|
|
g.add(board);
|
|
// Copper traces
|
|
for (let i = 0; i < 6; i++) {
|
|
const traceGeo = new THREE.BoxGeometry(0.6, 0.005, 0.03);
|
|
const trace = new THREE.Mesh(traceGeo, pcbCopper);
|
|
trace.position.y = 0.045;
|
|
trace.position.z = -0.3 + i * 0.12;
|
|
trace.rotation.y = (i % 2) * 0.3;
|
|
g.add(trace);
|
|
}
|
|
// Components (capacitors, ICs)
|
|
const comp1Geo = new THREE.BoxGeometry(0.12, 0.1, 0.08);
|
|
const comp1 = new THREE.Mesh(comp1Geo, new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.8 }));
|
|
comp1.position.set(0.15, 0.09, 0.1);
|
|
g.add(comp1);
|
|
const comp2Geo = new THREE.BoxGeometry(0.08, 0.06, 0.06);
|
|
const comp2 = new THREE.Mesh(comp2Geo, new THREE.MeshStandardMaterial({ color: 0x222244, roughness: 0.6 }));
|
|
comp2.position.set(-0.2, 0.07, -0.15);
|
|
g.add(comp2);
|
|
// Larger IC
|
|
const icGeo = new THREE.BoxGeometry(0.18, 0.05, 0.18);
|
|
const ic = new THREE.Mesh(icGeo, new THREE.MeshStandardMaterial({ color: 0x0a0a0a, metalness: 0.5, roughness: 0.4 }));
|
|
ic.position.set(-0.05, 0.065, 0.15);
|
|
g.add(ic);
|
|
// Capacitor (cylinder)
|
|
const capGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.14, 12);
|
|
const cap = new THREE.Mesh(capGeo, new THREE.MeshStandardMaterial({ color: 0x2244aa, roughness: 0.5 }));
|
|
cap.position.set(0.25, 0.11, -0.2);
|
|
g.add(cap);
|
|
// Second capacitor
|
|
const cap2Geo = new THREE.CylinderGeometry(0.035, 0.035, 0.1, 12);
|
|
const cap2 = new THREE.Mesh(cap2Geo, new THREE.MeshStandardMaterial({ color: 0x884422, roughness: 0.5 }));
|
|
cap2.position.set(-0.25, 0.09, 0.0);
|
|
g.add(cap2);
|
|
// Solder points
|
|
for (let i = 0; i < 8; i++) {
|
|
const solderGeo = new THREE.SphereGeometry(0.015, 8, 8);
|
|
const solder = new THREE.Mesh(solderGeo, new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.9 }));
|
|
const angle = (i / 8) * Math.PI * 2;
|
|
solder.position.set(Math.cos(angle) * 0.32, 0.045, Math.sin(angle) * 0.32);
|
|
g.add(solder);
|
|
}
|
|
g.position.y = parts.pcb.baseY;
|
|
micGroup.add(g);
|
|
})();
|
|
|
|
// ── BUILD BODY ──
|
|
(() => {
|
|
const g = parts.body.group;
|
|
// Main cylinder
|
|
const bodyGeo = new THREE.CylinderGeometry(0.55, 0.5, 2.0, 32);
|
|
const body = new THREE.Mesh(bodyGeo, metalDark);
|
|
g.add(body);
|
|
// Decorative rings
|
|
for (let i = 0; i < 3; i++) {
|
|
const ringGeo = new THREE.TorusGeometry(0.56, 0.015, 8, 48);
|
|
const ring = new THREE.Mesh(ringGeo, goldMat);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = 0.6 - i * 0.6;
|
|
g.add(ring);
|
|
}
|
|
// Visible screws
|
|
for (let i = 0; i < 4; i++) {
|
|
const screwGeo = new THREE.CylinderGeometry(0.025, 0.025, 0.04, 8);
|
|
const screw = new THREE.Mesh(screwGeo, new THREE.MeshStandardMaterial({ color: 0x999999, metalness: 0.95 }));
|
|
const angle = (i / 4) * Math.PI * 2;
|
|
screw.position.set(Math.cos(angle) * 0.56, -0.3, Math.sin(angle) * 0.56);
|
|
screw.rotation.z = Math.PI / 2;
|
|
screw.lookAt(0, -0.3, 0);
|
|
g.add(screw);
|
|
}
|
|
g.position.y = parts.body.baseY;
|
|
micGroup.add(g);
|
|
})();
|
|
|
|
// ── BUILD CONNECTOR ──
|
|
(() => {
|
|
const g = parts.connector.group;
|
|
// Base taper
|
|
const taperGeo = new THREE.CylinderGeometry(0.5, 0.35, 0.5, 32);
|
|
const taper = new THREE.Mesh(taperGeo, metalDark);
|
|
taper.position.y = 0.2;
|
|
g.add(taper);
|
|
// XLR housing
|
|
const xlrGeo = new THREE.CylinderGeometry(0.32, 0.32, 0.4, 32);
|
|
const xlr = new THREE.Mesh(xlrGeo, metalMid);
|
|
xlr.position.y = -0.2;
|
|
g.add(xlr);
|
|
// Gold ring
|
|
const ringGeo = new THREE.TorusGeometry(0.33, 0.02, 12, 48);
|
|
const ring = new THREE.Mesh(ringGeo, goldMat);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = -0.05;
|
|
g.add(ring);
|
|
// XLR pins
|
|
for (let i = 0; i < 3; i++) {
|
|
const pinGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.15, 8);
|
|
const pin = new THREE.Mesh(pinGeo, goldMat);
|
|
const angle = (i / 3) * Math.PI * 2 - Math.PI / 2;
|
|
pin.position.set(Math.cos(angle) * 0.12, -0.45, Math.sin(angle) * 0.12);
|
|
g.add(pin);
|
|
}
|
|
g.position.y = parts.connector.baseY;
|
|
micGroup.add(g);
|
|
})();
|
|
|
|
// Center the mic
|
|
micGroup.position.y = -0.5;
|
|
micGroup.rotation.x = 0.15;
|
|
micGroup.rotation.y = -0.4;
|
|
|
|
// ────────────────────────────────────────────
|
|
// SCROLL ANIMATION
|
|
// ────────────────────────────────────────────
|
|
const heroText = document.getElementById('hero-text');
|
|
const scrollHint = document.getElementById('scroll-hint');
|
|
const buySection = document.getElementById('buy-section');
|
|
const repairBadge = document.getElementById('repair-badge');
|
|
const progressDots = document.querySelectorAll('.progress-dot');
|
|
|
|
const labels = {
|
|
grille: document.getElementById('label-grille'),
|
|
capsule: document.getElementById('label-capsule'),
|
|
pcb: document.getElementById('label-pcb'),
|
|
body: document.getElementById('label-body'),
|
|
connector: document.getElementById('label-connector'),
|
|
};
|
|
|
|
// Easing
|
|
function easeInOutCubic(t) {
|
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
}
|
|
function easeOutQuint(t) {
|
|
return 1 - Math.pow(1 - t, 5);
|
|
}
|
|
function clamp(v, min, max) {
|
|
return Math.max(min, Math.min(max, v));
|
|
}
|
|
function mapRange(value, inMin, inMax, outMin, outMax) {
|
|
return outMin + (outMax - outMin) * clamp((value - inMin) / (inMax - inMin), 0, 1);
|
|
}
|
|
|
|
let scrollProgress = 0;
|
|
let autoRotate = 0;
|
|
let isSpinning = false;
|
|
|
|
function updateScene() {
|
|
const scrollY = window.scrollY;
|
|
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
|
|
scrollProgress = clamp(scrollY / maxScroll, 0, 1);
|
|
|
|
// ── PHASE MAPPING ──
|
|
// 0.00 - 0.08 : Hero (assembled)
|
|
// 0.08 - 0.55 : Explode parts one by one
|
|
// 0.55 - 0.70 : Fully exploded, all labels shown
|
|
// 0.70 - 0.82 : Reassemble quickly
|
|
// 0.82 - 1.00 : Spinning assembled + buy CTA
|
|
|
|
// Hero fade
|
|
const heroOpacity = mapRange(scrollProgress, 0, 0.08, 1, 0);
|
|
heroText.style.opacity = heroOpacity;
|
|
heroText.style.transform = `translateY(calc(-50% + ${scrollProgress * -100}px))`;
|
|
scrollHint.style.opacity = mapRange(scrollProgress, 0, 0.05, 1, 0);
|
|
|
|
// ── EXPLODE ANIMATION ──
|
|
const explodeStart = 0.08;
|
|
const explodeEnd = 0.55;
|
|
const reassembleStart = 0.70;
|
|
const reassembleEnd = 0.82;
|
|
|
|
let explodeFactor;
|
|
if (scrollProgress < explodeStart) {
|
|
explodeFactor = 0;
|
|
} else if (scrollProgress < explodeEnd) {
|
|
explodeFactor = easeInOutCubic(mapRange(scrollProgress, explodeStart, explodeEnd, 0, 1));
|
|
} else if (scrollProgress < reassembleStart) {
|
|
explodeFactor = 1;
|
|
} else if (scrollProgress < reassembleEnd) {
|
|
explodeFactor = 1 - easeOutQuint(mapRange(scrollProgress, reassembleStart, reassembleEnd, 0, 1));
|
|
} else {
|
|
explodeFactor = 0;
|
|
}
|
|
|
|
// Per-part staggered explosion
|
|
const partKeys = ['grille', 'capsule', 'pcb', 'body', 'connector'];
|
|
const stagger = [0, 0.04, 0.08, 0.12, 0.16];
|
|
|
|
partKeys.forEach((key, i) => {
|
|
const part = parts[key];
|
|
let partFactor;
|
|
if (scrollProgress < reassembleStart) {
|
|
const pStart = explodeStart + stagger[i];
|
|
const pEnd = pStart + (explodeEnd - explodeStart) * 0.6;
|
|
partFactor = easeInOutCubic(clamp((scrollProgress - pStart) / (pEnd - pStart), 0, 1));
|
|
} else {
|
|
partFactor = explodeFactor;
|
|
}
|
|
const y = part.baseY + (part.explodeY - part.baseY) * partFactor;
|
|
part.group.position.y = y;
|
|
|
|
// Slight rotation during explode for visual interest
|
|
if (key === 'pcb') {
|
|
part.group.rotation.y = partFactor * Math.PI * 0.15;
|
|
}
|
|
if (key === 'grille') {
|
|
part.group.rotation.z = partFactor * 0.1;
|
|
}
|
|
});
|
|
|
|
// ── LABEL VISIBILITY ──
|
|
const labelRanges = {
|
|
grille: [0.10, 0.25],
|
|
capsule: [0.18, 0.33],
|
|
pcb: [0.28, 0.50],
|
|
body: [0.38, 0.55],
|
|
connector: [0.48, 0.65],
|
|
};
|
|
|
|
Object.entries(labelRanges).forEach(([key, [start, end]]) => {
|
|
const label = labels[key];
|
|
const visible = scrollProgress >= start && scrollProgress <= end + 0.08;
|
|
const opacity = visible
|
|
? mapRange(scrollProgress, start, start + 0.04, 0, 1) * mapRange(scrollProgress, end, end + 0.08, 1, 0)
|
|
: 0;
|
|
label.style.opacity = clamp(opacity, 0, 1);
|
|
label.classList.toggle('visible', opacity > 0.1);
|
|
});
|
|
|
|
// Repair badge
|
|
const repairVisible = scrollProgress > 0.45 && scrollProgress < 0.68;
|
|
repairBadge.classList.toggle('visible', repairVisible);
|
|
|
|
// ── CAMERA ──
|
|
if (scrollProgress < 0.70) {
|
|
// Pan camera during explode
|
|
camera.position.x = mapRange(scrollProgress, 0, 0.55, 0, -1);
|
|
camera.position.y = mapRange(scrollProgress, 0, 0.55, 2, 1);
|
|
camera.position.z = mapRange(scrollProgress, 0, 0.55, 8, 9);
|
|
} else {
|
|
// Return for reassembly
|
|
camera.position.x = mapRange(scrollProgress, 0.70, 0.85, -1, 0);
|
|
camera.position.y = mapRange(scrollProgress, 0.70, 0.85, 1, 1.5);
|
|
camera.position.z = mapRange(scrollProgress, 0.70, 0.85, 9, 7.5);
|
|
}
|
|
camera.lookAt(0, 0.3, 0);
|
|
|
|
// ── MICRO ROTATION ──
|
|
if (scrollProgress < 0.70) {
|
|
micGroup.rotation.y = -0.4 + scrollProgress * 1.2;
|
|
isSpinning = false;
|
|
} else {
|
|
isSpinning = true;
|
|
}
|
|
|
|
// ── BUY SECTION ──
|
|
const buyVisible = scrollProgress > 0.85;
|
|
buySection.classList.toggle('visible', buyVisible);
|
|
|
|
// ── PROGRESS DOTS ──
|
|
const sections = [0, 0.12, 0.24, 0.36, 0.50, 0.72, 0.88];
|
|
progressDots.forEach((dot, i) => {
|
|
const isActive = scrollProgress >= sections[i] && (i === sections.length - 1 || scrollProgress < sections[i + 1]);
|
|
dot.classList.toggle('active', isActive);
|
|
});
|
|
}
|
|
|
|
// ── RENDER LOOP ──
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
if (isSpinning) {
|
|
autoRotate += 0.012;
|
|
micGroup.rotation.y = autoRotate;
|
|
} else {
|
|
autoRotate = micGroup.rotation.y;
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// ── EVENTS ──
|
|
window.addEventListener('scroll', updateScene, { passive: true });
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
updateScene();
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html>
|