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>
1497 lines
44 KiB
HTML
1497 lines
44 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 — 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>
|