talas-group/07_CONTENUS_MARKETING/microphone-page.html

988 lines
28 KiB
HTML
Raw Permalink Normal View History

<!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>