talas-group/07_CONTENUS_MARKETING/microphone-v2.html
senke 66471934af Initial commit: Talas Group project management & documentation
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>
2026-04-04 20:10:41 +02:00

1497 lines
44 KiB
HTML
Raw 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="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RĒVO — Engineered to Last</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=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=JetBrains+Mono:wght@300;400;500;700&family=Outfit:wght@200;300;400;500;600&display=swap');
:root {
--bg: #060606;
--bg-card: rgba(12,12,12,0.8);
--text: #e2ddd5;
--text-dim: #5c574f;
--text-mid: #8a847b;
--copper: #c4875c;
--copper-bright: #e8a872;
--copper-glow: rgba(196,135,92,0.12);
--gold: #d4a574;
--green: #5fa85f;
--green-bg: rgba(95,168,95,0.08);
--green-border: rgba(95,168,95,0.2);
--line: rgba(255,255,255,0.04);
--display: 'Cormorant Garamond', serif;
--body: 'Outfit', sans-serif;
--mono: 'JetBrains Mono', monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scrollbar-width: none; }
html::-webkit-scrollbar { display: none; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--body);
overflow-x: hidden;
}
.scroll-container { height: 700vh; }
/* ── CANVAS ── */
#scene-container {
position: fixed;
inset: 0;
z-index: 1;
}
/* ── AMBIENT GRID ── */
.ambient-grid {
position: fixed;
inset: 0;
z-index: 2;
pointer-events: none;
opacity: 0.025;
background-image:
linear-gradient(var(--line) 1px, transparent 1px),
linear-gradient(90deg, var(--line) 1px, transparent 1px);
background-size: 60px 60px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, black 20%, transparent 70%);
}
/* ── GRAIN OVERLAY ── */
.grain {
position: fixed;
inset: -50%;
width: 200%; height: 200%;
z-index: 998;
pointer-events: none;
opacity: 0.018;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='5' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-size: 256px;
}
/* ── VIGNETTE ── */
.vignette {
position: fixed;
inset: 0;
z-index: 3;
pointer-events: none;
background: radial-gradient(ellipse 65% 65% at 55% 45%, transparent 0%, rgba(0,0,0,0.75) 100%);
}
/* ── NAVIGATION ── */
.nav {
position: fixed;
top: 0; left: 0; width: 100%;
z-index: 100;
padding: 32px 56px;
display: flex;
justify-content: space-between;
align-items: center;
transition: opacity 0.5s;
}
.nav-brand {
display: flex;
align-items: baseline;
gap: 12px;
}
.nav-logo {
font-family: var(--display);
font-size: 24px;
font-weight: 300;
letter-spacing: 8px;
color: var(--text);
text-transform: uppercase;
}
.nav-tag {
font-family: var(--mono);
font-size: 8px;
font-weight: 300;
letter-spacing: 4px;
color: var(--text-dim);
text-transform: uppercase;
border: 1px solid rgba(255,255,255,0.06);
padding: 3px 10px;
border-radius: 2px;
}
.nav-links {
display: flex;
gap: 40px;
list-style: none;
}
.nav-links a {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 2.5px;
color: var(--text-dim);
text-decoration: none;
text-transform: uppercase;
transition: color 0.4s;
position: relative;
}
.nav-links a::after {
content: '';
position: absolute;
bottom: -4px; left: 0;
width: 0; height: 1px;
background: var(--copper);
transition: width 0.4s;
}
.nav-links a:hover { color: var(--text); }
.nav-links a:hover::after { width: 100%; }
/* ── HERO ── */
.hero-content {
position: fixed;
z-index: 20;
pointer-events: none;
left: 56px;
top: 50%;
transform: translateY(-50%);
transition: opacity 0.6s, transform 0.6s;
}
.hero-eyebrow {
font-family: var(--mono);
font-size: 9px;
font-weight: 400;
letter-spacing: 5px;
text-transform: uppercase;
color: var(--copper);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 14px;
}
.hero-eyebrow::before {
content: '';
width: 40px; height: 1px;
background: linear-gradient(90deg, var(--copper), transparent);
}
.hero-h1 {
font-family: var(--display);
font-size: clamp(42px, 5.5vw, 80px);
font-weight: 300;
line-height: 1.05;
letter-spacing: -0.5px;
color: var(--text);
max-width: 480px;
}
.hero-h1 em {
font-style: italic;
font-weight: 300;
color: var(--copper-bright);
}
.hero-sub {
margin-top: 28px;
font-family: var(--body);
font-size: 15px;
font-weight: 200;
line-height: 1.8;
color: var(--text-mid);
max-width: 360px;
letter-spacing: 0.3px;
}
.hero-badges {
margin-top: 32px;
display: flex;
gap: 12px;
}
.hero-badge {
font-family: var(--mono);
font-size: 9px;
font-weight: 400;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-dim);
border: 1px solid rgba(255,255,255,0.06);
padding: 6px 14px;
border-radius: 3px;
}
/* ── SCROLL CTA ── */
.scroll-cta {
position: fixed;
bottom: 44px; left: 50%;
transform: translateX(-50%);
z-index: 20;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
transition: opacity 0.5s;
}
.scroll-cta-text {
font-family: var(--mono);
font-size: 8px;
font-weight: 300;
letter-spacing: 5px;
text-transform: uppercase;
color: var(--text-dim);
}
.scroll-cta-line {
width: 1px; height: 48px;
background: rgba(255,255,255,0.08);
position: relative;
overflow: hidden;
border-radius: 1px;
}
.scroll-cta-line::after {
content: '';
position: absolute; top: -100%; left: 0;
width: 100%; height: 50%;
background: linear-gradient(to bottom, transparent, var(--copper));
animation: dropLine 2.2s ease-in-out infinite;
}
@keyframes dropLine {
0%, 100% { top: -50%; opacity: 0; }
30% { opacity: 1; }
70% { opacity: 1; }
90% { top: 120%; opacity: 0; }
}
/* ── PART ANNOTATIONS ── */
.annotation {
position: fixed;
z-index: 20;
pointer-events: none;
opacity: 0;
transition: opacity 0.7s cubic-bezier(0.16,1,0.3,1), transform 0.7s cubic-bezier(0.16,1,0.3,1);
}
.annotation.from-left { transform: translateX(-40px); }
.annotation.from-right { transform: translateX(40px); }
.annotation.visible { opacity: 1; transform: translateX(0); }
.anno-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.anno-num {
font-family: var(--mono);
font-size: 9px;
font-weight: 500;
color: var(--bg);
background: var(--copper);
width: 22px; height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.anno-tag {
font-family: var(--mono);
font-size: 8px;
font-weight: 400;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--copper);
}
.anno-line {
width: 48px; height: 1px;
background: linear-gradient(90deg, var(--copper), transparent);
margin-bottom: 10px;
}
.annotation.from-right .anno-line {
background: linear-gradient(270deg, var(--copper), transparent);
margin-left: auto;
}
.anno-name {
font-family: var(--display);
font-size: clamp(26px, 3vw, 42px);
font-weight: 300;
line-height: 1.15;
color: var(--text);
margin-bottom: 12px;
}
.anno-desc {
font-family: var(--body);
font-size: 13px;
font-weight: 300;
line-height: 1.85;
color: var(--text-mid);
max-width: 300px;
letter-spacing: 0.2px;
}
.anno-specs {
margin-top: 18px;
display: flex;
gap: 24px;
}
.anno-spec {
display: flex;
flex-direction: column;
gap: 3px;
}
.anno-spec-val {
font-family: var(--mono);
font-size: 20px;
font-weight: 500;
color: var(--copper-bright);
line-height: 1;
}
.anno-spec-key {
font-family: var(--mono);
font-size: 7px;
font-weight: 400;
letter-spacing: 3px;
text-transform: uppercase;
color: var(--text-dim);
}
.anno-repairability {
margin-top: 16px;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
background: var(--green-bg);
border: 1px solid var(--green-border);
border-radius: 4px;
}
.anno-repair-icon {
font-size: 11px;
}
.anno-repair-text {
font-family: var(--mono);
font-size: 8px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--green);
}
/* ── PROGRESS RAIL ── */
.progress-rail {
position: fixed;
right: 36px;
top: 50%;
transform: translateY(-50%);
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
.progress-rail-track {
width: 2px;
height: 140px;
background: rgba(255,255,255,0.04);
border-radius: 2px;
position: relative;
overflow: hidden;
}
.progress-rail-fill {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 0%;
background: linear-gradient(to bottom, var(--copper), var(--copper-bright));
border-radius: 2px;
transition: height 0.15s;
box-shadow: 0 0 8px var(--copper-glow);
}
.progress-rail-labels {
position: absolute;
right: 14px; top: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.prl {
font-family: var(--mono);
font-size: 7px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-dim);
opacity: 0.3;
transition: opacity 0.3s, color 0.3s;
text-align: right;
white-space: nowrap;
}
.prl.active {
opacity: 1;
color: var(--copper);
}
/* ── REPAIR SCORE ── */
.repair-floating {
position: fixed;
z-index: 25;
bottom: 80px; left: 56px;
pointer-events: none;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s, transform 0.6s;
}
.repair-floating.visible {
opacity: 1;
transform: translateY(0);
}
.rf-card {
background: var(--bg-card);
backdrop-filter: blur(24px);
border: 1px solid var(--green-border);
border-radius: 10px;
padding: 20px 28px;
display: flex;
align-items: center;
gap: 18px;
}
.rf-score-ring {
width: 56px; height: 56px;
position: relative;
}
.rf-score-ring svg {
width: 100%; height: 100%;
transform: rotate(-90deg);
}
.rf-score-ring circle {
fill: none;
stroke-width: 3;
}
.rf-ring-bg { stroke: rgba(255,255,255,0.05); }
.rf-ring-fill {
stroke: var(--green);
stroke-dasharray: 160;
stroke-dashoffset: 0;
stroke-linecap: round;
filter: drop-shadow(0 0 4px rgba(95,168,95,0.3));
}
.rf-score-num {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--mono);
font-size: 14px;
font-weight: 700;
color: var(--green);
}
.rf-info {
display: flex;
flex-direction: column;
gap: 3px;
}
.rf-title {
font-family: var(--mono);
font-size: 8px;
font-weight: 500;
letter-spacing: 3px;
text-transform: uppercase;
color: var(--green);
}
.rf-desc {
font-family: var(--body);
font-size: 12px;
font-weight: 300;
color: var(--text-dim);
}
/* ── BUY SECTION ── */
.buy-section {
position: fixed;
bottom: 0; left: 0; width: 100%;
z-index: 30;
pointer-events: none;
opacity: 0;
transform: translateY(60px);
transition: opacity 0.8s cubic-bezier(0.16,1,0.3,1), transform 0.8s cubic-bezier(0.16,1,0.3,1);
display: flex;
justify-content: center;
padding: 0 56px 52px;
}
.buy-section.visible {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
.buy-card {
background: var(--bg-card);
backdrop-filter: blur(30px);
border: 1px solid rgba(196,135,92,0.12);
border-radius: 14px;
padding: 28px 44px;
display: flex;
align-items: center;
gap: 44px;
max-width: 720px;
width: 100%;
position: relative;
overflow: hidden;
}
.buy-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%; height: 1px;
background: linear-gradient(90deg, transparent, var(--copper), transparent);
opacity: 0.4;
}
.buy-left {
flex: 1;
}
.buy-pretitle {
font-family: var(--mono);
font-size: 8px;
font-weight: 400;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--text-dim);
margin-bottom: 6px;
}
.buy-name {
font-family: var(--display);
font-size: 26px;
font-weight: 300;
color: var(--text);
margin-bottom: 4px;
}
.buy-price {
font-family: var(--mono);
font-size: 32px;
font-weight: 700;
color: var(--copper-bright);
line-height: 1;
}
.buy-meta {
margin-top: 8px;
display: flex;
gap: 16px;
}
.buy-meta span {
font-family: var(--mono);
font-size: 8px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-dim);
}
.buy-btn {
font-family: var(--mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--bg);
background: linear-gradient(135deg, var(--copper), var(--copper-bright));
border: none;
border-radius: 60px;
padding: 20px 44px;
cursor: pointer;
transition: transform 0.4s cubic-bezier(0.16,1,0.3,1), box-shadow 0.4s;
white-space: nowrap;
position: relative;
overflow: hidden;
}
.buy-btn::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, transparent 40%, rgba(255,255,255,0.2) 50%, transparent 60%);
transform: translateX(-100%);
transition: transform 0.6s;
}
.buy-btn:hover {
transform: scale(1.06);
box-shadow: 0 10px 40px rgba(196,135,92,0.25);
}
.buy-btn:hover::after {
transform: translateX(100%);
}
/* ── CONNECTOR LINES (SVG overlay) ── */
#connector-lines {
position: fixed;
inset: 0;
z-index: 15;
pointer-events: none;
}
.conn-line {
stroke: var(--copper);
stroke-width: 0.5;
fill: none;
opacity: 0;
stroke-dasharray: 300;
stroke-dashoffset: 300;
transition: opacity 0.5s, stroke-dashoffset 1.2s cubic-bezier(0.16,1,0.3,1);
}
.conn-line.visible {
opacity: 0.4;
stroke-dashoffset: 0;
}
.conn-dot {
fill: var(--copper);
opacity: 0;
transition: opacity 0.5s;
}
.conn-dot.visible { opacity: 0.7; }
/* ── FLOATING PARTICLES ── */
#particle-canvas {
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
}
/* ── COUNTERS ANIMATION ── */
@keyframes fadeInSpec {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div class="grain"></div>
<div class="vignette"></div>
<div class="ambient-grid"></div>
<canvas id="particle-canvas"></canvas>
<!-- CONNECTOR LINES SVG -->
<svg id="connector-lines" xmlns="http://www.w3.org/2000/svg">
<line class="conn-line" id="cl-grille" x1="0" y1="0" x2="0" y2="0"/>
<circle class="conn-dot" id="cd-grille" r="3"/>
<line class="conn-line" id="cl-capsule" x1="0" y1="0" x2="0" y2="0"/>
<circle class="conn-dot" id="cd-capsule" r="3"/>
<line class="conn-line" id="cl-pcb" x1="0" y1="0" x2="0" y2="0"/>
<circle class="conn-dot" id="cd-pcb" r="3"/>
<line class="conn-line" id="cl-body" x1="0" y1="0" x2="0" y2="0"/>
<circle class="conn-dot" id="cd-body" r="3"/>
<line class="conn-line" id="cl-connector" x1="0" y1="0" x2="0" y2="0"/>
<circle class="conn-dot" id="cd-connector" r="3"/>
</svg>
<!-- NAV -->
<nav class="nav" id="nav">
<div class="nav-brand">
<div class="nav-logo">Rēvo</div>
<div class="nav-tag">audio</div>
</div>
<ul class="nav-links">
<li><a href="#">Manifeste</a></li>
<li><a href="#">Réparation</a></li>
<li><a href="#">Open Source</a></li>
<li><a href="#">Acheter</a></li>
</ul>
</nav>
<!-- 3D -->
<div id="scene-container"></div>
<!-- HERO -->
<div class="hero-content" id="hero-content">
<div class="hero-eyebrow">Microphone condensateur</div>
<h1 class="hero-h1">Conçu pour<br>être <em>réparé</em></h1>
<p class="hero-sub">Chaque pièce se démonte, se remplace, se recycle. Zéro colle. Zéro clip cassable. Zéro obsolescence.</p>
<div class="hero-badges">
<div class="hero-badge">Open-Hardware</div>
<div class="hero-badge">Garantie 10 ans</div>
<div class="hero-badge">Pièces à vie</div>
</div>
</div>
<!-- SCROLL CTA -->
<div class="scroll-cta" id="scroll-cta">
<div class="scroll-cta-line"></div>
<div class="scroll-cta-text">Découvrir l'intérieur</div>
</div>
<!-- ANNOTATIONS -->
<div class="annotation from-right" id="anno-grille" style="top: 18%; right: 56px; max-width: 340px;">
<div class="anno-header">
<div class="anno-num">01</div>
<div class="anno-tag">Grille acoustique</div>
</div>
<div class="anno-line"></div>
<div class="anno-name">Acier inoxydable<br>micro-perforé</div>
<div class="anno-desc">Fixation magnétique — retrait sans outil en 2 secondes. Résiste aux chocs, lavable à l'eau. Remplaçable à vie pour 12 €.</div>
<div class="anno-specs">
<div class="anno-spec"><span class="anno-spec-val">304L</span><span class="anno-spec-key">Inox</span></div>
<div class="anno-spec"><span class="anno-spec-val">0.8mm</span><span class="anno-spec-key">Perforations</span></div>
<div class="anno-spec"><span class="anno-spec-val">2s</span><span class="anno-spec-key">Démontage</span></div>
</div>
<div class="anno-repairability"><span class="anno-repair-icon"></span><span class="anno-repair-text">Sans outil</span></div>
</div>
<div class="annotation from-left" id="anno-capsule" style="top: 28%; left: 56px; max-width: 340px;">
<div class="anno-header">
<div class="anno-num">02</div>
<div class="anno-tag">Capsule condensateur</div>
</div>
<div class="anno-line"></div>
<div class="anno-name">Membrane<br>plaquée or 34mm</div>
<div class="anno-desc">Directivité cardioïde, connecteur enfichable standardisé. Remplacement en 30 secondes. Upgrade possible vers membrane omni.</div>
<div class="anno-specs">
<div class="anno-spec"><span class="anno-spec-val">34mm</span><span class="anno-spec-key">Diamètre</span></div>
<div class="anno-spec"><span class="anno-spec-val">20Hz</span><span class="anno-spec-key">Low cut</span></div>
<div class="anno-spec"><span class="anno-spec-val">20kHz</span><span class="anno-spec-key">High end</span></div>
</div>
<div class="anno-repairability"><span class="anno-repair-icon"></span><span class="anno-repair-text">Connecteur enfichable</span></div>
</div>
<div class="annotation from-right" id="anno-pcb" style="top: 38%; right: 56px; max-width: 360px;">
<div class="anno-header">
<div class="anno-num">03</div>
<div class="anno-tag">Électronique</div>
</div>
<div class="anno-line"></div>
<div class="anno-name">PCB modulaire<br>open-source</div>
<div class="anno-desc">Préampli classe A discret, composants traversants soudables à la main. Schémas, Gerber et BOM publiés sous CERN-OHL-S v2. Modifiable, auditable, réparable par quiconque sait tenir un fer.</div>
<div class="anno-specs">
<div class="anno-spec"><span class="anno-spec-val">7dB</span><span class="anno-spec-key">Sensibilité</span></div>
<div class="anno-spec"><span class="anno-spec-val">THT</span><span class="anno-spec-key">Composants</span></div>
<div class="anno-spec"><span class="anno-spec-val">OHL-S</span><span class="anno-spec-key">Licence</span></div>
</div>
<div class="anno-repairability"><span class="anno-repair-icon"></span><span class="anno-repair-text">Soudure manuelle</span></div>
</div>
<div class="annotation from-left" id="anno-body" style="top: 52%; left: 56px; max-width: 340px;">
<div class="anno-header">
<div class="anno-num">04</div>
<div class="anno-tag">Corps</div>
</div>
<div class="anno-line"></div>
<div class="anno-name">Fût aluminium<br>6061-T6 usiné CNC</div>
<div class="anno-desc">Anodisation noir mat, assemblage par 4 vis apparentes. Aucune colle, aucun clip. Conçu pour survivre à une décennie de tournées.</div>
<div class="anno-specs">
<div class="anno-spec"><span class="anno-spec-val">6061</span><span class="anno-spec-key">Alliage</span></div>
<div class="anno-spec"><span class="anno-spec-val">CNC</span><span class="anno-spec-key">Usinage</span></div>
<div class="anno-spec"><span class="anno-spec-val">4 vis</span><span class="anno-spec-key">Fixation</span></div>
</div>
<div class="anno-repairability"><span class="anno-repair-icon"></span><span class="anno-repair-text">Tournevis cruciforme</span></div>
</div>
<div class="annotation from-right" id="anno-connector" style="top: 64%; right: 56px; max-width: 340px;">
<div class="anno-header">
<div class="anno-num">05</div>
<div class="anno-tag">Connectique</div>
</div>
<div class="anno-line"></div>
<div class="anno-name">Embase XLR<br>Neutrik plaqué or</div>
<div class="anno-desc">Vissée au châssis, pas soudée. Remplacement sans fer à souder. Compatible 48V phantom standard.</div>
<div class="anno-specs">
<div class="anno-spec"><span class="anno-spec-val">XLR-3</span><span class="anno-spec-key">Standard</span></div>
<div class="anno-spec"><span class="anno-spec-val">Au</span><span class="anno-spec-key">Contacts</span></div>
<div class="anno-spec"><span class="anno-spec-val">48V</span><span class="anno-spec-key">Phantom</span></div>
</div>
<div class="anno-repairability"><span class="anno-repair-icon"></span><span class="anno-repair-text">Vissé, pas soudé</span></div>
</div>
<!-- REPAIR SCORE -->
<div class="repair-floating" id="repair-floating">
<div class="rf-card">
<div class="rf-score-ring">
<svg viewBox="0 0 56 56">
<circle class="rf-ring-bg" cx="28" cy="28" r="24"/>
<circle class="rf-ring-fill" cx="28" cy="28" r="24"/>
</svg>
<div class="rf-score-num">10</div>
</div>
<div class="rf-info">
<div class="rf-title">Indice de réparabilité</div>
<div class="rf-desc">Score maximal — chaque pièce est<br>remplaçable individuellement</div>
</div>
</div>
</div>
<!-- PROGRESS RAIL -->
<div class="progress-rail" id="progress-rail">
<div class="progress-rail-track">
<div class="progress-rail-fill" id="prf"></div>
<div class="progress-rail-labels">
<span class="prl active" data-s="0">Intro</span>
<span class="prl" data-s="1">Grille</span>
<span class="prl" data-s="2">Capsule</span>
<span class="prl" data-s="3">PCB</span>
<span class="prl" data-s="4">Corps</span>
<span class="prl" data-s="5">XLR</span>
<span class="prl" data-s="6">Achat</span>
</div>
</div>
</div>
<!-- BUY -->
<div class="buy-section" id="buy-section">
<div class="buy-card">
<div class="buy-left">
<div class="buy-pretitle">Rēvo Condenser Microphone</div>
<div class="buy-name">Édition Fondateur</div>
<div class="buy-price">249 €</div>
<div class="buy-meta">
<span>Garantie 10 ans</span>
<span>·</span>
<span>Pièces à vie</span>
<span>·</span>
<span>Schémas inclus</span>
</div>
</div>
<button class="buy-btn">Précommander</button>
</div>
</div>
<!-- SCROLL SPACER -->
<div class="scroll-container"></div>
<script>
// ═══════════════════════════════════════════════
// FLOATING PARTICLES
// ═══════════════════════════════════════════════
const pCanvas = document.getElementById('particle-canvas');
const pCtx = pCanvas.getContext('2d');
let particles = [];
function initParticles() {
pCanvas.width = window.innerWidth;
pCanvas.height = window.innerHeight;
particles = [];
const count = Math.min(60, Math.floor(window.innerWidth / 25));
for (let i = 0; i < count; i++) {
particles.push({
x: Math.random() * pCanvas.width,
y: Math.random() * pCanvas.height,
vx: (Math.random() - 0.5) * 0.15,
vy: (Math.random() - 0.5) * 0.1 - 0.08,
size: Math.random() * 1.5 + 0.3,
opacity: Math.random() * 0.25 + 0.05,
pulse: Math.random() * Math.PI * 2,
});
}
}
function drawParticles() {
pCtx.clearRect(0, 0, pCanvas.width, pCanvas.height);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.pulse += 0.01;
if (p.y < -10) { p.y = pCanvas.height + 10; p.x = Math.random() * pCanvas.width; }
if (p.x < -10) p.x = pCanvas.width + 10;
if (p.x > pCanvas.width + 10) p.x = -10;
const o = p.opacity * (0.6 + 0.4 * Math.sin(p.pulse));
pCtx.beginPath();
pCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
pCtx.fillStyle = `rgba(196,135,92,${o})`;
pCtx.fill();
});
}
initParticles();
// ═══════════════════════════════════════════════
// THREE.JS SCENE
// ═══════════════════════════════════════════════
const container = document.getElementById('scene-container');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 9);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// ── LIGHTS ──
scene.add(new THREE.AmbientLight(0x887766, 0.2));
const key = new THREE.DirectionalLight(0xffeedd, 1.3);
key.position.set(4, 6, 5);
key.castShadow = true;
key.shadow.mapSize.set(1024, 1024);
key.shadow.bias = -0.001;
scene.add(key);
const fill = new THREE.DirectionalLight(0x6688aa, 0.35);
fill.position.set(-4, 3, -3);
scene.add(fill);
const rim = new THREE.SpotLight(0xc4875c, 0.8, 20, Math.PI / 6, 0.5);
rim.position.set(-2, -2, -5);
rim.target.position.set(0, 0, 0);
scene.add(rim);
scene.add(rim.target);
const topSpot = new THREE.SpotLight(0xffeedd, 0.5, 15, Math.PI / 8, 0.7);
topSpot.position.set(0, 8, 2);
topSpot.target.position.set(0, 0, 0);
topSpot.castShadow = true;
scene.add(topSpot);
scene.add(topSpot.target);
const bottomFill = new THREE.PointLight(0xc4875c, 0.2, 8);
bottomFill.position.set(0, -3, 3);
scene.add(bottomFill);
// ── REFLECTION PLANE ──
const planeGeo = new THREE.PlaneGeometry(20, 20);
const planeMat = new THREE.MeshStandardMaterial({
color: 0x060606,
metalness: 0.95,
roughness: 0.6,
transparent: true,
opacity: 0.5,
});
const plane = new THREE.Mesh(planeGeo, planeMat);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -3.8;
plane.receiveShadow = true;
scene.add(plane);
// ── MATERIALS ──
const mat = {
body: new THREE.MeshStandardMaterial({ color: 0x141414, metalness: 0.92, roughness: 0.22 }),
bodyInner: new THREE.MeshStandardMaterial({ color: 0x1a1a1a, metalness: 0.85, roughness: 0.3 }),
grille: new THREE.MeshStandardMaterial({ color: 0x282828, metalness: 0.95, roughness: 0.18 }),
grilleWire: new THREE.MeshStandardMaterial({ color: 0x1e1e1e, metalness: 0.9, roughness: 0.25, wireframe: true }),
gold: new THREE.MeshStandardMaterial({ color: 0xc4875c, metalness: 0.96, roughness: 0.12 }),
goldBright: new THREE.MeshStandardMaterial({ color: 0xd4a574, metalness: 0.96, roughness: 0.1 }),
capsule: new THREE.MeshStandardMaterial({ color: 0x1c1c1c, metalness: 0.8, roughness: 0.35 }),
pcb: new THREE.MeshStandardMaterial({ color: 0x0f3a1a, metalness: 0.25, roughness: 0.55 }),
copper: new THREE.MeshStandardMaterial({ color: 0xb87333, metalness: 0.92, roughness: 0.25 }),
ic: new THREE.MeshStandardMaterial({ color: 0x080808, metalness: 0.5, roughness: 0.35 }),
cap: new THREE.MeshStandardMaterial({ color: 0x1a3a6a, metalness: 0.3, roughness: 0.5 }),
capBrown: new THREE.MeshStandardMaterial({ color: 0x6a3a1a, metalness: 0.3, roughness: 0.5 }),
silver: new THREE.MeshStandardMaterial({ color: 0x999999, metalness: 0.95, roughness: 0.2 }),
connector: new THREE.MeshStandardMaterial({ color: 0x1a1a1a, metalness: 0.88, roughness: 0.28 }),
};
// ── MIC GROUP ──
const micGroup = new THREE.Group();
scene.add(micGroup);
const parts = {
grille: { g: new THREE.Group(), baseY: 2.65, explodeY: 5.6, rotX: 0, rotZ: 0 },
capsule: { g: new THREE.Group(), baseY: 1.85, explodeY: 3.4, rotX: 0, rotZ: 0 },
pcb: { g: new THREE.Group(), baseY: 0.7, explodeY: 0.7, rotX: 0, rotZ: 0 },
body: { g: new THREE.Group(), baseY: -0.6, explodeY: -2.1, rotX: 0, rotZ: 0 },
connector: { g: new THREE.Group(), baseY: -2.2, explodeY: -4.6, rotX: 0, rotZ: 0 },
};
// ═══ GRILLE ═══
(() => {
const g = parts.grille.g;
// Outer dome
const dome = new THREE.Mesh(
new THREE.SphereGeometry(0.68, 48, 32, 0, Math.PI * 2, 0, Math.PI * 0.52),
mat.grille
);
g.add(dome);
// Inner wireframe
const inner = new THREE.Mesh(
new THREE.SphereGeometry(0.62, 20, 14, 0, Math.PI * 2, 0, Math.PI * 0.48),
mat.grilleWire
);
g.add(inner);
// Second inner layer
const inner2 = new THREE.Mesh(
new THREE.SphereGeometry(0.56, 12, 8, 0, Math.PI * 2, 0, Math.PI * 0.45),
new THREE.MeshStandardMaterial({ color: 0x161616, metalness: 0.85, roughness: 0.3, wireframe: true })
);
g.add(inner2);
// Base ring
const ring = new THREE.Mesh(new THREE.TorusGeometry(0.66, 0.05, 16, 64), mat.gold);
ring.rotation.x = Math.PI / 2;
ring.position.y = -0.16;
g.add(ring);
// Thin accent ring
const ring2 = new THREE.Mesh(new THREE.TorusGeometry(0.69, 0.015, 12, 64), mat.goldBright);
ring2.rotation.x = Math.PI / 2;
ring2.position.y = -0.2;
g.add(ring2);
g.position.y = parts.grille.baseY;
micGroup.add(g);
})();
// ═══ CAPSULE ═══
(() => {
const g = parts.capsule.g;
const housing = new THREE.Mesh(new THREE.CylinderGeometry(0.56, 0.62, 0.65, 48), mat.capsule);
g.add(housing);
// Diaphragm
const dia = new THREE.Mesh(new THREE.CylinderGeometry(0.42, 0.42, 0.025, 48), mat.goldBright);
dia.position.y = 0.2;
g.add(dia);
// Backplate
const bp = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.4, 0.04, 48), mat.copper);
bp.position.y = 0.1;
g.add(bp);
// Rings
[0.3, -0.15].forEach(y => {
const r = new THREE.Mesh(new THREE.TorusGeometry(0.58, 0.02, 12, 48), mat.gold);
r.rotation.x = Math.PI / 2; r.position.y = y; g.add(r);
});
// Contact pins at bottom
for (let i = 0; i < 5; i++) {
const pin = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.2, 8), mat.gold);
const a = (i / 5) * Math.PI * 2;
pin.position.set(Math.cos(a) * 0.25, -0.42, Math.sin(a) * 0.25);
g.add(pin);
}
g.position.y = parts.capsule.baseY;
micGroup.add(g);
})();
// ═══ PCB ═══
(() => {
const g = parts.pcb.g;
// Board
const board = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.07, 0.9), mat.pcb);
g.add(board);
// Second smaller board underneath
const board2 = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.05, 0.5), mat.pcb);
board2.position.y = -0.08;
g.add(board2);
// Copper traces top
const tracePositions = [
{ w: 0.7, z: -0.35, r: 0 }, { w: 0.65, z: -0.22, r: 0.15 },
{ w: 0.5, z: -0.08, r: -0.1 }, { w: 0.6, z: 0.05, r: 0.05 },
{ w: 0.55, z: 0.18, r: -0.08 }, { w: 0.7, z: 0.32, r: 0 },
];
tracePositions.forEach(t => {
const trace = new THREE.Mesh(new THREE.BoxGeometry(t.w, 0.004, 0.025), mat.copper);
trace.position.set(0, 0.04, t.z);
trace.rotation.y = t.r;
g.add(trace);
});
// Cross traces
for (let i = 0; i < 4; i++) {
const ct = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.004, 0.5), mat.copper);
ct.position.set(-0.25 + i * 0.18, 0.04, 0);
g.add(ct);
}
// IC chip
const ic = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.06, 0.2), mat.ic);
ic.position.set(0, 0.065, 0.1);
g.add(ic);
// IC marking (tiny gold dot)
const icDot = new THREE.Mesh(new THREE.SphereGeometry(0.015, 8, 8), mat.gold);
icDot.position.set(-0.06, 0.1, 0.05);
g.add(icDot);
// Electrolytic caps
[{ x: 0.28, z: -0.22, h: 0.18, r: 0.045, m: mat.cap },
{ x: -0.3, z: 0.25, h: 0.14, r: 0.038, m: mat.capBrown },
{ x: 0.32, z: 0.2, h: 0.12, r: 0.032, m: mat.cap }].forEach(c => {
const cap = new THREE.Mesh(new THREE.CylinderGeometry(c.r, c.r, c.h, 16), c.m);
cap.position.set(c.x, 0.04 + c.h / 2, c.z);
g.add(cap);
// Cap top ring
const cr = new THREE.Mesh(new THREE.TorusGeometry(c.r * 0.8, 0.005, 8, 16), mat.silver);
cr.rotation.x = Math.PI / 2;
cr.position.set(c.x, 0.04 + c.h, c.z);
g.add(cr);
});
// SMD resistors
for (let i = 0; i < 8; i++) {
const smd = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.02, 0.03), mat.ic);
const a = (i / 8) * Math.PI * 2;
smd.position.set(Math.cos(a) * 0.18, 0.045, Math.sin(a) * 0.18);
smd.rotation.y = a;
g.add(smd);
}
// Solder pads
for (let i = 0; i < 12; i++) {
const sp = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.008, 8), mat.silver);
const a = (i / 12) * Math.PI * 2;
sp.position.set(Math.cos(a) * 0.36, 0.04, Math.sin(a) * 0.36);
g.add(sp);
}
// Connector header
const header = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.08, 0.06), new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.7 }));
header.position.set(-0.2, 0.075, -0.32);
g.add(header);
for (let i = 0; i < 5; i++) {
const hp = new THREE.Mesh(new THREE.CylinderGeometry(0.008, 0.008, 0.12, 6), mat.gold);
hp.position.set(-0.24 + i * 0.025, 0.095, -0.32);
g.add(hp);
}
g.position.y = parts.pcb.baseY;
micGroup.add(g);
})();
// ═══ BODY ═══
(() => {
const g = parts.body.g;
// Main body
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.55, 2.2, 48), mat.body);
body.castShadow = true;
g.add(body);
// Internal bore
const bore = new THREE.Mesh(
new THREE.CylinderGeometry(0.48, 0.45, 2.22, 32),
new THREE.MeshStandardMaterial({ color: 0x0c0c0c, metalness: 0.7, roughness: 0.4, side: THREE.BackSide })
);
g.add(bore);
// Gold accent rings
const ringYs = [0.85, 0.2, -0.45, -0.9];
ringYs.forEach((y, i) => {
const thickness = i === 0 ? 0.035 : 0.02;
const r = new THREE.Mesh(new THREE.TorusGeometry(0.605 - (i > 1 ? 0.01 : 0), thickness, 12, 64), mat.gold);
r.rotation.x = Math.PI / 2;
r.position.y = y;
g.add(r);
});
// Knurling texture band
for (let i = 0; i < 80; i++) {
const a = (i / 80) * Math.PI * 2;
const knurl = new THREE.Mesh(new THREE.BoxGeometry(0.008, 0.35, 0.015), mat.bodyInner);
knurl.position.set(Math.cos(a) * 0.605, -0.2, Math.sin(a) * 0.605);
knurl.rotation.y = -a;
g.add(knurl);
}
// Visible screws
for (let i = 0; i < 4; i++) {
const a = (i / 4) * Math.PI * 2 + Math.PI / 4;
// Screw head
const head = new THREE.Mesh(new THREE.CylinderGeometry(0.032, 0.032, 0.02, 6), mat.silver);
head.position.set(Math.cos(a) * 0.61, -0.65, Math.sin(a) * 0.61);
head.lookAt(0, -0.65, 0);
g.add(head);
// Slot
const slot = new THREE.Mesh(
new THREE.BoxGeometry(0.04, 0.003, 0.008),
new THREE.MeshStandardMaterial({ color: 0x333333 })
);
slot.position.copy(head.position);
slot.lookAt(0, -0.65, 0);
g.add(slot);
}
g.position.y = parts.body.baseY;
micGroup.add(g);
})();
// ═══ CONNECTOR ═══
(() => {
const g = parts.connector.g;
// Taper
const taper = new THREE.Mesh(new THREE.CylinderGeometry(0.55, 0.38, 0.55, 48), mat.connector);
taper.position.y = 0.2;
taper.castShadow = true;
g.add(taper);
// XLR barrel
const xlr = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 0.45, 48), mat.connector);
xlr.position.y = -0.2;
g.add(xlr);
// Gold rings
[0.0, -0.42].forEach(y => {
const r = new THREE.Mesh(new THREE.TorusGeometry(0.36, 0.02, 12, 48), mat.gold);
r.rotation.x = Math.PI / 2; r.position.y = y; g.add(r);
});
// XLR pins
for (let i = 0; i < 3; i++) {
const a = (i / 3) * Math.PI * 2 - Math.PI / 2;
const pin = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.022, 0.18, 8), mat.goldBright);
pin.position.set(Math.cos(a) * 0.14, -0.52, Math.sin(a) * 0.14);
g.add(pin);
}
// Ground pin (center)
const gndPin = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.22, 8), mat.silver);
gndPin.position.set(0, -0.53, 0);
g.add(gndPin);
// Internal thread lines
for (let i = 0; i < 3; i++) {
const tr = new THREE.Mesh(new THREE.TorusGeometry(0.33, 0.005, 6, 32), mat.silver);
tr.rotation.x = Math.PI / 2;
tr.position.y = -0.1 - i * 0.12;
g.add(tr);
}
g.position.y = parts.connector.baseY;
micGroup.add(g);
})();
micGroup.position.y = -0.3;
micGroup.rotation.x = 0.12;
micGroup.rotation.y = -0.5;
// ═══════════════════════════════════════════════
// SCROLL ENGINE
// ═══════════════════════════════════════════════
const heroEl = document.getElementById('hero-content');
const scrollCta = document.getElementById('scroll-cta');
const buyEl = document.getElementById('buy-section');
const repairEl = document.getElementById('repair-floating');
const prfEl = document.getElementById('prf');
const prlEls = document.querySelectorAll('.prl');
const annoEls = {
grille: document.getElementById('anno-grille'),
capsule: document.getElementById('anno-capsule'),
pcb: document.getElementById('anno-pcb'),
body: document.getElementById('anno-body'),
connector: document.getElementById('anno-connector'),
};
// Connector line elements
const clEls = {}, cdEls = {};
['grille','capsule','pcb','body','connector'].forEach(k => {
clEls[k] = document.getElementById('cl-' + k);
cdEls[k] = document.getElementById('cd-' + k);
});
function ease(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2,3)/2; }
function easeOut5(t) { return 1 - Math.pow(1-t,5); }
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function lerp(a, b, t) { return a + (b - a) * clamp(t, 0, 1); }
function map(v, a, b, c, d) { return c + (d-c) * clamp((v-a)/(b-a), 0, 1); }
let sp = 0, autoRot = 0, spinning = false;
// Project 3D to screen
function project(vec3) {
const v = vec3.clone().project(camera);
return {
x: (v.x * 0.5 + 0.5) * window.innerWidth,
y: (-v.y * 0.5 + 0.5) * window.innerHeight,
};
}
function updateConnectorLines() {
const partScreenPos = {};
const partKeys = ['grille','capsule','pcb','body','connector'];
partKeys.forEach(k => {
const wp = new THREE.Vector3();
parts[k].g.getWorldPosition(wp);
partScreenPos[k] = project(wp);
});
// Annotation positions
const annoPositions = {
grille: { side: 'right', el: annoEls.grille },
capsule: { side: 'left', el: annoEls.capsule },
pcb: { side: 'right', el: annoEls.pcb },
body: { side: 'left', el: annoEls.body },
connector: { side: 'right', el: annoEls.connector },
};
partKeys.forEach(k => {
const pp = partScreenPos[k];
const ap = annoPositions[k];
const rect = ap.el.getBoundingClientRect();
const visible = ap.el.classList.contains('visible');
let lx, ly;
if (ap.side === 'right') {
lx = rect.left;
ly = rect.top + rect.height * 0.35;
} else {
lx = rect.right;
ly = rect.top + rect.height * 0.35;
}
clEls[k].setAttribute('x1', pp.x);
clEls[k].setAttribute('y1', pp.y);
clEls[k].setAttribute('x2', lx);
clEls[k].setAttribute('y2', ly);
cdEls[k].setAttribute('cx', pp.x);
cdEls[k].setAttribute('cy', pp.y);
clEls[k].classList.toggle('visible', visible);
cdEls[k].classList.toggle('visible', visible);
});
}
function updateScene() {
const scrollY = window.scrollY;
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
sp = clamp(scrollY / maxScroll, 0, 1);
// Progress rail
prfEl.style.height = (sp * 100) + '%';
const sections = [0, 0.10, 0.20, 0.32, 0.44, 0.56, 0.80];
prlEls.forEach((el, i) => {
const active = sp >= sections[i] && (i === sections.length - 1 || sp < sections[i+1]);
el.classList.toggle('active', active);
});
// Hero
heroEl.style.opacity = map(sp, 0, 0.07, 1, 0);
heroEl.style.transform = `translateY(calc(-50% + ${sp * -120}px))`;
scrollCta.style.opacity = map(sp, 0, 0.04, 1, 0);
// ── EXPLODE PHASES ──
const eStart = 0.07;
const eEnd = 0.58;
const rStart = 0.72;
const rEnd = 0.82;
let ef;
if (sp < eStart) ef = 0;
else if (sp < eEnd) ef = ease(map(sp, eStart, eEnd, 0, 1));
else if (sp < rStart) ef = 1;
else if (sp < rEnd) ef = 1 - easeOut5(map(sp, rStart, rEnd, 0, 1));
else ef = 0;
const partKeys = ['grille','capsule','pcb','body','connector'];
const stagger = [0, 0.035, 0.07, 0.11, 0.15];
partKeys.forEach((k, i) => {
const p = parts[k];
let pf;
if (sp < rStart) {
const ps = eStart + stagger[i];
const pe = ps + (eEnd - eStart) * 0.55;
pf = ease(clamp((sp - ps) / (pe - ps), 0, 1));
} else {
pf = ef;
}
p.g.position.y = lerp(p.baseY, p.explodeY, pf);
// Individual part rotations during explode
if (k === 'pcb') {
p.g.rotation.y = pf * 0.25;
p.g.rotation.x = pf * -0.08;
}
if (k === 'grille') p.g.rotation.z = pf * 0.08;
if (k === 'connector') p.g.rotation.z = pf * -0.05;
});
// ── ANNOTATIONS ──
const annoRanges = {
grille: [0.10, 0.22],
capsule: [0.18, 0.32],
pcb: [0.28, 0.48],
body: [0.38, 0.54],
connector: [0.48, 0.64],
};
Object.entries(annoRanges).forEach(([k, [s, e]]) => {
const el = annoEls[k];
const vis = sp >= s && sp <= e + 0.06;
const o = vis
? map(sp, s, s + 0.035, 0, 1) * map(sp, e, e + 0.06, 1, 0)
: 0;
el.style.opacity = clamp(o, 0, 1);
el.classList.toggle('visible', o > 0.1);
});
// Repair badge
const repVis = sp > 0.42 && sp < 0.66;
repairEl.classList.toggle('visible', repVis);
// ── CAMERA CHOREOGRAPHY ──
if (sp < 0.72) {
camera.position.x = lerp(0, -0.8, map(sp, 0, 0.55, 0, 1));
camera.position.y = lerp(2, 0.8, map(sp, 0, 0.55, 0, 1));
camera.position.z = lerp(9, 10.5, map(sp, 0, 0.55, 0, 1));
} else {
camera.position.x = lerp(-0.8, 0, map(sp, 0.72, 0.85, 0, 1));
camera.position.y = lerp(0.8, 1.2, map(sp, 0.72, 0.85, 0, 1));
camera.position.z = lerp(10.5, 8, map(sp, 0.72, 0.85, 0, 1));
}
camera.lookAt(0, 0.2, 0);
// Mic base rotation
if (sp < 0.72) {
micGroup.rotation.y = -0.5 + sp * 1.5;
spinning = false;
} else {
spinning = true;
}
// Buy
buyEl.classList.toggle('visible', sp > 0.87);
// Update connector lines
updateConnectorLines();
}
// ── RENDER LOOP ──
function animate() {
requestAnimationFrame(animate);
if (spinning) {
autoRot += 0.008;
micGroup.rotation.y = autoRot;
} else {
autoRot = micGroup.rotation.y;
}
drawParticles();
renderer.render(scene, camera);
}
window.addEventListener('scroll', updateScene, { passive: true });
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
pCanvas.width = window.innerWidth;
pCanvas.height = window.innerHeight;
});
updateScene();
animate();
</script>
</body>
</html>