Files
electron-rare.github.io/src/components/WebGLBackground.tsx
T
Clément SAILLANT 177458fd07 feat: 50+ transition components — STM32, FPGA, ARM NPU, DSP, selfs
Transition components between each section:
- HERO→ABOUT: STM32F030 (Cortex-M0) + 8 passives + LEDs
- ABOUT→CASES: 74HC595 shift register + 2 selfs (inductors) + passives
- CASES→MEDIA: XC7A35T FPGA Artix-7 + DONE/INIT LEDs + passives
- MEDIA→SPRINTS: STM32N6 ARM Neural Unit + self + decoupling
- SPRINTS→CONTACT: TMS320F DSP C2000 + PWM/ADC LEDs + self

All with silkscreen ref designators and component values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 07:18:54 +00:00

887 lines
36 KiB
TypeScript

import { useRef, useMemo, useEffect, useState } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { Text } from '@react-three/drei';
import { EffectComposer, Bloom, ChromaticAberration, Scanline } from '@react-three/postprocessing';
import { BlendFunction } from 'postprocessing';
import * as THREE from 'three';
/* ===================================================================
PCB JOURNEY — Scroll follows copper traces on a circuit board
The camera travels along PCB traces as the user scrolls.
Current flows through the traces as glowing particles.
Site sections are "components" soldered onto the board.
=================================================================== */
const COLORS = {
copper: new THREE.Color('#c87533'),
copperBright: new THREE.Color('#e8a040'),
substrate: new THREE.Color('#1a3a1a'),
substrateDark: new THREE.Color('#0d1f0d'),
solder: new THREE.Color('#c0c0c0'),
current: new THREE.Color('#5bd1d8'),
currentWarm: new THREE.Color('#f1c27a'),
silkscreen: new THREE.Color('#e8e8d0'),
via: new THREE.Color('#888888'),
};
/* ---------- Scroll state ---------- */
let scrollProgress = 0;
/* ---------- PCB Trace path (the main route the camera follows) ---------- */
const TRACE_POINTS = [
// Start — top of board (wide open)
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(3, 0, -6),
new THREE.Vector3(6, 0, -10),
// First bend (Hero section)
new THREE.Vector3(10, 0, -12),
new THREE.Vector3(10, 0, -18),
// Long run to About
new THREE.Vector3(6, 0, -26),
new THREE.Vector3(0, 0, -32),
// Via + layer change (Cases)
new THREE.Vector3(-4, -0.1, -38),
new THREE.Vector3(-8, -0.1, -44),
// Right angle bend (Photos)
new THREE.Vector3(-8, -0.1, -52),
new THREE.Vector3(-4, 0, -58),
// Long straight (Sprints)
new THREE.Vector3(3, 0, -64),
new THREE.Vector3(8, 0, -70),
// Final destination (Contact)
new THREE.Vector3(8, 0, -78),
new THREE.Vector3(4, 0, -84),
new THREE.Vector3(0, 0, -90),
];
function createTraceCurve() {
return new THREE.CatmullRomCurve3(TRACE_POINTS, false, 'catmullrom', 0.3);
}
/* ---------- Main copper trace ---------- */
function CopperTrace() {
const curve = useMemo(() => createTraceCurve(), []);
const tubeGeo = useMemo(() => {
return new THREE.TubeGeometry(curve, 200, 0.08, 8, false);
}, [curve]);
return (
<group>
{/* Main trace — copper tube */}
<mesh geometry={tubeGeo}>
<meshStandardMaterial
color={COLORS.copper}
emissive={COLORS.copper}
emissiveIntensity={0.3}
metalness={0.8}
roughness={0.3}
/>
</mesh>
{/* Glow around trace */}
<mesh geometry={tubeGeo}>
<meshStandardMaterial
color={COLORS.copperBright}
transparent
opacity={0.08}
emissive={COLORS.copperBright}
emissiveIntensity={0.5}
/>
</mesh>
</group>
);
}
/* ---------- Secondary traces (decorative, branch off main) ---------- */
function SecondaryTraces() {
const traces = useMemo(() => {
const paths: THREE.Vector3[][] = [];
// Branch traces at various points along the main route
const branches = [
// Hero area branches
[new THREE.Vector3(5, 0, -8), new THREE.Vector3(5, 0, -4), new THREE.Vector3(8, 0, -2)],
[new THREE.Vector3(7, 0, -11), new THREE.Vector3(10, 0, -7), new THREE.Vector3(13, 0, -6)],
// About area
[new THREE.Vector3(3, 0, -28), new THREE.Vector3(3, 0, -24), new THREE.Vector3(6, 0, -22)],
[new THREE.Vector3(-2, 0, -31), new THREE.Vector3(-5, 0, -27), new THREE.Vector3(-7, 0, -26)],
// Cases area
[new THREE.Vector3(-5, -0.1, -40), new THREE.Vector3(-3, -0.1, -36), new THREE.Vector3(0, -0.1, -35)],
[new THREE.Vector3(-8, -0.1, -46), new THREE.Vector3(-11, -0.1, -44), new THREE.Vector3(-13, -0.1, -42)],
// Sprints area
[new THREE.Vector3(6, 0, -65), new THREE.Vector3(6, 0, -61), new THREE.Vector3(9, 0, -59)],
[new THREE.Vector3(2, 0, -67), new THREE.Vector3(-1, 0, -63), new THREE.Vector3(-4, 0, -62)],
// Contact area
[new THREE.Vector3(5, 0, -80), new THREE.Vector3(8, 0, -76), new THREE.Vector3(10, 0, -75)],
[new THREE.Vector3(1, 0, -85), new THREE.Vector3(-2, 0, -82), new THREE.Vector3(-5, 0, -81)],
];
branches.forEach(pts => {
const curve = new THREE.CatmullRomCurve3(pts, false, 'catmullrom', 0.5);
const tubePts = curve.getPoints(30);
paths.push(tubePts);
});
return paths;
}, []);
return (
<group>
{traces.map((pts, i) => {
const geo = new THREE.BufferGeometry().setFromPoints(pts);
return (
<line key={i} geometry={geo}>
<lineBasicMaterial
color={COLORS.copper}
transparent
opacity={0.25}
linewidth={1}
/>
</line>
);
})}
</group>
);
}
/* ---------- Current flow particles along the trace ---------- */
function CurrentFlow({ count = 60 }) {
const ref = useRef<THREE.InstancedMesh>(null);
const dummy = useMemo(() => new THREE.Object3D(), []);
const curve = useMemo(() => createTraceCurve(), []);
const phases = useMemo(() => Array.from({ length: count }, () => Math.random()), [count]);
useFrame(({ clock }) => {
if (!ref.current) return;
const t = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
// Each particle flows along the curve
const progress = ((phases[i] + t * 0.06) % 1);
const pos = curve.getPointAt(progress);
// Add slight jitter
dummy.position.set(
pos.x + (Math.random() - 0.5) * 0.05,
pos.y + 0.02 + Math.sin(t * 3 + i) * 0.02,
pos.z + (Math.random() - 0.5) * 0.05,
);
// Size based on proximity to camera/scroll position
const distToScroll = Math.abs(progress - scrollProgress);
const brightness = Math.max(0.2, 1 - distToScroll * 3);
dummy.scale.setScalar(0.03 * brightness + 0.01);
dummy.updateMatrix();
ref.current.setMatrixAt(i, dummy.matrix);
}
ref.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh ref={ref} args={[undefined, undefined, count]}>
<sphereGeometry args={[1, 6, 6]} />
<meshStandardMaterial
color={COLORS.current}
emissive={COLORS.current}
emissiveIntensity={3}
transparent
opacity={0.9}
/>
</instancedMesh>
);
}
/* ---------- Warm current (second layer) ---------- */
function WarmCurrentFlow({ count = 30 }) {
const ref = useRef<THREE.InstancedMesh>(null);
const dummy = useMemo(() => new THREE.Object3D(), []);
const curve = useMemo(() => createTraceCurve(), []);
const phases = useMemo(() => Array.from({ length: count }, () => Math.random()), [count]);
useFrame(({ clock }) => {
if (!ref.current) return;
const t = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
const progress = ((phases[i] + t * 0.04 + 0.5) % 1);
const pos = curve.getPointAt(progress);
dummy.position.set(pos.x, pos.y + 0.03, pos.z);
const distToScroll = Math.abs(progress - scrollProgress);
const brightness = Math.max(0.1, 1 - distToScroll * 4);
dummy.scale.setScalar(0.02 * brightness + 0.005);
dummy.updateMatrix();
ref.current.setMatrixAt(i, dummy.matrix);
}
ref.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh ref={ref} args={[undefined, undefined, count]}>
<sphereGeometry args={[1, 6, 6]} />
<meshStandardMaterial
color={COLORS.currentWarm}
emissive={COLORS.currentWarm}
emissiveIntensity={2}
transparent
opacity={0.7}
/>
</instancedMesh>
);
}
/* ---------- PCB Substrate (green board) ---------- */
function Substrate() {
return (
<group>
{/* Main board */}
<mesh position={[0, -0.15, -45]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[35, 110]} />
<meshStandardMaterial
color={COLORS.substrateDark}
roughness={0.9}
metalness={0.05}
/>
</mesh>
{/* Solder mask grid pattern */}
{Array.from({ length: 20 }).map((_, i) => (
<mesh key={`h${i}`} position={[0, -0.14, -2 - i * 4.5]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[33, 0.01]} />
<meshBasicMaterial color={COLORS.substrate} transparent opacity={0.15} />
</mesh>
))}
{Array.from({ length: 14 }).map((_, i) => (
<mesh key={`v${i}`} position={[-13 + i * 2, -0.14, -19]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[0.01, 105]} />
<meshBasicMaterial color={COLORS.substrate} transparent opacity={0.15} />
</mesh>
))}
</group>
);
}
/* ---------- Vias (through-hole connections) ---------- */
function Vias() {
const positions: [number, number, number][] = [
[10, 0, -14], [8, 0, -18], [3, 0, -28],
[-3, 0, -36], [-7, 0, -42], [-6, 0, -50],
[0, 0, -60], [5, 0, -68], [3, 0, -80],
// Extra decorative vias
[12, 0, -10], [-9, 0, -28], [9, 0, -52],
[-10, 0, -65], [10, 0, -82], [-3, 0, -88],
];
return (
<group>
{positions.map(([x, y, z], i) => (
<group key={i} position={[x, y, z]}>
{/* Via ring */}
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.12, 0.2, 16]} />
<meshStandardMaterial
color={COLORS.solder}
metalness={0.9}
roughness={0.2}
emissive={COLORS.via}
emissiveIntensity={0.2}
/>
</mesh>
{/* Via hole */}
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[0.1, 16]} />
<meshBasicMaterial color="#050505" />
</mesh>
</group>
))}
</group>
);
}
/* ---------- 3D Electronic components on PCB ---------- */
/* IC / Microcontroller (black rectangle with pins) */
function ICComponent({ position, size = [2, 0.3, 1.2], label, color = '#1a1a1a' }: {
position: [number, number, number]; size?: [number, number, number]; label: string; color?: string;
}) {
const groupRef = useRef<THREE.Group>(null);
const pins = Math.floor(size[0] / 0.3);
useFrame(({ clock }) => {
if (!groupRef.current) return;
const cameraZ = -scrollProgress * 90;
const dist = Math.abs(position[2] - cameraZ);
const glow = Math.max(0, 1 - dist / 6);
// Subtle float when active
groupRef.current.position.y = position[1] + glow * Math.sin(clock.getElapsedTime() * 2) * 0.03;
});
return (
<group ref={groupRef} position={position}>
{/* IC body */}
<mesh position={[0, size[1] / 2, 0]}>
<boxGeometry args={size} />
<meshStandardMaterial color={color} roughness={0.6} metalness={0.1} />
</mesh>
{/* Dot (pin 1 marker) */}
<mesh position={[-size[0] / 2 + 0.15, size[1] + 0.01, -size[2] / 2 + 0.15]} rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[0.05, 8]} />
<meshBasicMaterial color="#e0e0e0" />
</mesh>
{/* Silkscreen label on IC body */}
<Text
position={[0, size[1] + 0.02, 0]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.12}
font="/assets/fonts/manrope-regular.ttf"
color="#e8e8d0"
anchorX="center"
anchorY="middle"
material-transparent
material-opacity={0.6}
>
{label}
</Text>
{/* Pins — both sides */}
{Array.from({ length: pins }).map((_, i) => (
<group key={`pins-${i}`}>
<mesh position={[-size[0] / 2 + 0.15 + i * 0.3, 0.05, -size[2] / 2 - 0.08]}>
<boxGeometry args={[0.06, 0.02, 0.15]} />
<meshStandardMaterial color={COLORS.solder} metalness={0.9} roughness={0.2} />
</mesh>
<mesh position={[-size[0] / 2 + 0.15 + i * 0.3, 0.05, size[2] / 2 + 0.08]}>
<boxGeometry args={[0.06, 0.02, 0.15]} />
<meshStandardMaterial color={COLORS.solder} metalness={0.9} roughness={0.2} />
</mesh>
</group>
))}
{/* Solder pads glow */}
<mesh position={[0, -0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[size[0] + 0.5, size[2] + 0.5]} />
<meshStandardMaterial color={COLORS.copperBright} transparent opacity={0.04} emissive={COLORS.current} emissiveIntensity={0.2} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
/* Capacitor (cylinder) */
function Capacitor({ position, radius = 0.25, height = 0.5, color = '#2a4a8a' }: {
position: [number, number, number]; radius?: number; height?: number; color?: string;
}) {
const ref = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
if (!ref.current) return;
const cameraZ = -scrollProgress * 90;
const dist = Math.abs(position[2] - cameraZ);
const glow = Math.max(0, 1 - dist / 6);
(ref.current.material as THREE.MeshStandardMaterial).emissiveIntensity = glow * 0.5;
});
return (
<group position={position}>
<mesh ref={ref} position={[0, height / 2, 0]}>
<cylinderGeometry args={[radius, radius, height, 16]} />
<meshStandardMaterial color={color} roughness={0.4} metalness={0.2} emissive={COLORS.current} emissiveIntensity={0} />
</mesh>
{/* Top marking */}
<mesh position={[0, height + 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[radius * 0.7, 16]} />
<meshStandardMaterial color="#333" metalness={0.5} roughness={0.3} />
</mesh>
{/* Stripe */}
<mesh position={[0, height * 0.7, radius + 0.01]}>
<planeGeometry args={[radius * 1.2, height * 0.15]} />
<meshBasicMaterial color={COLORS.silkscreen} transparent opacity={0.3} />
</mesh>
</group>
);
}
/* Resistor (small rectangle with color bands) */
function Resistor({ position, rotation = [0, 0, 0] }: { position: [number, number, number]; rotation?: [number, number, number] }) {
return (
<group position={position} rotation={rotation}>
<mesh position={[0, 0.04, 0]}>
<boxGeometry args={[0.5, 0.08, 0.2]} />
<meshStandardMaterial color="#2a2a2a" roughness={0.7} />
</mesh>
{/* Solder pads */}
<mesh position={[-0.22, 0.02, 0]}>
<boxGeometry args={[0.1, 0.04, 0.22]} />
<meshStandardMaterial color={COLORS.solder} metalness={0.9} roughness={0.2} />
</mesh>
<mesh position={[0.22, 0.02, 0]}>
<boxGeometry args={[0.1, 0.04, 0.22]} />
<meshStandardMaterial color={COLORS.solder} metalness={0.9} roughness={0.2} />
</mesh>
</group>
);
}
/* LED (small dome with glow) */
function LED({ position, color = '#5bd1d8' }: { position: [number, number, number]; color?: string }) {
const ref = useRef<THREE.Mesh>(null);
const threeColor = useMemo(() => new THREE.Color(color), [color]);
useFrame(({ clock }) => {
if (!ref.current) return;
const t = clock.getElapsedTime();
const cameraZ = -scrollProgress * 90;
const dist = Math.abs(position[2] - cameraZ);
const proximity = Math.max(0, 1 - dist / 5);
(ref.current.material as THREE.MeshStandardMaterial).emissiveIntensity = proximity * (1.5 + Math.sin(t * 3) * 0.5);
});
return (
<group position={position}>
<mesh ref={ref}>
<sphereGeometry args={[0.08, 12, 8, 0, Math.PI * 2, 0, Math.PI / 2]} />
<meshStandardMaterial color={color} emissive={threeColor} emissiveIntensity={0} transparent opacity={0.9} />
</mesh>
{/* LED pad */}
<mesh position={[0, -0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[0.12, 8]} />
<meshStandardMaterial color={COLORS.solder} metalness={0.8} roughness={0.3} />
</mesh>
</group>
);
}
/* Silkscreen text (PCB printed labels) */
function Silk({ position, text, size = 0.15, color = '#e8e8d0', opacity = 0.5, rotation = [-Math.PI / 2, 0, 0] as [number, number, number] }: {
position: [number, number, number]; text: string; size?: number; color?: string; opacity?: number; rotation?: [number, number, number];
}) {
return (
<Text
position={position}
rotation={rotation}
fontSize={size}
font="/assets/fonts/manrope-regular.ttf"
color={color}
anchorX="center"
anchorY="middle"
material-transparent
material-opacity={opacity}
>
{text}
</Text>
);
}
/* Section title — larger, glows when camera is near */
function SectionLabel({ position, text, subtitle }: { position: [number, number, number]; text: string; subtitle?: string }) {
const ref = useRef<THREE.Group>(null);
useFrame(() => {
if (!ref.current) return;
const cameraZ = -scrollProgress * 90;
const dist = Math.abs(position[2] - cameraZ);
const glow = Math.max(0, 1 - dist / 6);
ref.current.children.forEach(child => {
if ((child as any).material) {
(child as any).material.opacity = 0.15 + glow * 0.7;
}
});
});
return (
<group ref={ref}>
<Text
position={[position[0], position[1] + 0.02, position[2]]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.35}
font="/assets/fonts/manrope-regular.ttf"
color="#5bd1d8"
anchorX="center"
anchorY="middle"
material-transparent
material-opacity={0.15}
letterSpacing={0.1}
>
{text}
</Text>
{subtitle && (
<Text
position={[position[0], position[1] + 0.02, position[2] + 0.55]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.12}
font="/assets/fonts/manrope-regular.ttf"
color="#e8e8d0"
anchorX="center"
anchorY="middle"
material-transparent
material-opacity={0.1}
letterSpacing={0.05}
>
{subtitle}
</Text>
)}
</group>
);
}
/* All components assembled on the PCB */
function PCBComponents() {
return (
<group>
{/* ── HERO — Main MCU ── */}
<SectionLabel position={[10, 0.01, -8]} text="L'ELECTRON RARE" subtitle="Systemes electroniques specifiques" />
<ICComponent position={[10, 0, -14]} size={[2.5, 0.35, 1.5]} label="U1 — ESP32-S3" />
<Silk position={[8, 0.02, -12.5]} text="R1" size={0.08} />
<Silk position={[8, 0.02, -15.5]} text="R2" size={0.08} />
<Silk position={[11.5, 0.02, -11]} text="D1" size={0.08} />
<Silk position={[11.5, 0.02, -13.5]} text="D2" size={0.08} />
<LED position={[11.5, 0, -12]} color="#5bd1d8" />
<LED position={[11.5, 0, -13]} color="#30d158" />
<Resistor position={[8, 0, -13]} />
<Resistor position={[8, 0, -15]} />
{/* ── ABOUT — Approche & expertise ── */}
<SectionLabel position={[1, 0.01, -26]} text="APPROCHE" subtitle="Diagnostic · Conception · Mise au point" />
<ICComponent position={[0, 0, -32]} size={[1.8, 0.25, 0.8]} label="U2 — LM358N" color="#222" />
<Silk position={[3, 0.02, -30]} text="C1 100uF" size={0.08} />
<Silk position={[-2, 0.02, -32]} text="C2 10nF" size={0.08} />
<Silk position={[2, 0.02, -34]} text="R3 10k" size={0.08} />
<Silk position={[-1, 0.02, -28]} text="R4 4k7" size={0.08} />
<Capacitor position={[3, 0, -30.5]} radius={0.3} height={0.6} color="#8B4513" />
<Capacitor position={[-2, 0, -33]} radius={0.2} height={0.4} color="#2a4a8a" />
<Resistor position={[2, 0, -34]} rotation={[0, 0.5, 0]} />
<Resistor position={[-1, 0, -29]} rotation={[0, -0.3, 0]} />
<LED position={[2.5, 0, -31]} color="#f1c27a" />
<Silk position={[2.5, 0.02, -30.5]} text="D3" size={0.08} />
{/* ── CASES — Cas concrets ── */}
<SectionLabel position={[-6, 0.01, -38]} text="CAS CONCRETS" subtitle="Audio · Industrie · Formation" />
<ICComponent position={[-8, -0.1, -44]} size={[1.5, 0.4, 1]} label="Q1 — IRFZ44N" color="#1a1a2a" />
<Silk position={[-5, 0.02, -40]} text="C3 470uF/35V" size={0.08} />
<Silk position={[-10, 0.02, -47]} text="C4 220uF/16V" size={0.08} />
<Capacitor position={[-5, -0.1, -42]} radius={0.4} height={0.8} color="#1a3a1a" />
<Capacitor position={[-10, -0.1, -47]} radius={0.35} height={0.7} color="#8B4513" />
<Resistor position={[-7, -0.1, -47]} />
<Silk position={[-7, 0.02, -47.5]} text="R5 0R1" size={0.08} />
<LED position={[-9, -0.1, -43]} color="#ff6b6b" />
<Silk position={[-9, 0.02, -42.5]} text="D4 PWR" size={0.08} />
{/* ── MEDIA — Photos & videos ── */}
<SectionLabel position={[-4, 0.01, -52]} text="TERRAIN" subtitle="Photos · Videos · Realisations" />
<ICComponent position={[-4, 0, -56]} size={[2, 0.5, 0.8]} label="J1 — USB-C 3.1" color="#333" />
<LED position={[-2, 0, -56]} color="#5bd1d8" />
<LED position={[-6, 0, -56]} color="#30d158" />
<Silk position={[-2, 0.02, -55.5]} text="TX" size={0.08} />
<Silk position={[-6, 0.02, -55.5]} text="RX" size={0.08} />
{/* ── SPRINTS — Missions ── */}
<SectionLabel position={[5, 0.01, -62]} text="MISSIONS" subtitle="Diagnostic · Prototype · Mission complete" />
<ICComponent position={[6, 0, -68]} size={[1.2, 0.3, 0.6]} label="U3 — LM7805CT" color="#1a1a1a" />
<Silk position={[4, 0.02, -66]} text="C5 100nF" size={0.08} />
<Silk position={[8, 0.02, -69]} text="C6 100nF" size={0.08} />
<Silk position={[5, 0.02, -71]} text="R6 220R" size={0.08} />
<Capacitor position={[4, 0, -66.5]} radius={0.25} height={0.5} color="#2a4a8a" />
<Capacitor position={[8, 0, -69.5]} radius={0.25} height={0.5} color="#2a4a8a" />
<Resistor position={[5, 0, -71]} rotation={[0, 0.8, 0]} />
<LED position={[7, 0, -66]} color="#b6d18f" />
<Silk position={[7, 0.02, -65.5]} text="D5 OK" size={0.08} />
{/* ── CONTACT — Energy storage ── */}
<SectionLabel position={[2, 0.01, -80]} text="CONTACT" subtitle="contact@lelectronrare.fr" />
<Capacitor position={[2, 0, -84]} radius={0.5} height={1} color="#4a2a8a" />
<Silk position={[2, 0.02, -83]} text="C7 1000uF/25V" size={0.08} />
<Capacitor position={[-1, 0, -86]} radius={0.4} height={0.8} color="#8B4513" />
<Silk position={[-1, 0.02, -85]} text="C8 470uF/16V" size={0.08} />
<LED position={[4, 0, -84]} color="#ff6b35" />
<Silk position={[4, 0.02, -83.5]} text="D6" size={0.08} />
<LED position={[-0.5, 0, -84]} color="#5bd1d8" />
<Silk position={[-0.5, 0.02, -83.5]} text="D7" size={0.08} />
<Resistor position={[0, 0, -82]} rotation={[0, 1.2, 0]} />
<Silk position={[0, 0.02, -81.5]} text="R7 1k" size={0.08} />
{/* ── Transition components between sections (~50 total) ── */}
{/* ── HERO → ABOUT : STM32F030 + passives ── */}
<ICComponent position={[7, 0, -18]} size={[1.8, 0.3, 1]} label="U6 — STM32F030" color="#1a1a2a" />
<Silk position={[7, 0.02, -16.5]} text="ARM Cortex-M0" size={0.08} opacity={0.2} />
<Capacitor position={[9, 0, -17]} radius={0.2} height={0.4} color="#2a4a8a" />
<Silk position={[9, 0.02, -16.5]} text="C11 100nF" size={0.07} opacity={0.25} />
<Capacitor position={[5, 0, -19]} radius={0.15} height={0.3} color="#8B4513" />
<Silk position={[5, 0.02, -18.5]} text="C12 22pF" size={0.07} opacity={0.25} />
<Resistor position={[8.5, 0, -20]} rotation={[0, 0.3, 0]} />
<Silk position={[8.5, 0.02, -19.5]} text="R10 10k" size={0.07} opacity={0.25} />
<Resistor position={[4, 0, -21]} rotation={[0, -0.5, 0]} />
<Silk position={[4, 0.02, -20.5]} text="R11 4k7" size={0.07} opacity={0.25} />
<LED position={[6, 0, -19.5]} color="#5bd1d8" />
<Silk position={[6, 0.02, -19]} text="D8 SWD" size={0.07} opacity={0.25} />
<Capacitor position={[3, 0, -23]} radius={0.2} height={0.4} color="#2a4a8a" />
<Resistor position={[6.5, 0, -22]} rotation={[0, 0.8, 0]} />
<Resistor position={[5, 0, -24]} rotation={[0, -0.2, 0]} />
<LED position={[8, 0, -23]} color="#30d158" />
{/* ── ABOUT → CASES : 74HC595 + self + résistances ── */}
<ICComponent position={[-2, 0, -36]} size={[1.2, 0.2, 0.6]} label="U4 — 74HC595" color="#1a1a2a" />
<Silk position={[-2, 0.02, -35]} text="SHIFT REG" size={0.08} opacity={0.2} />
{/* Self (inductor) — cylindre avec bandes */}
<Capacitor position={[1, 0, -35]} radius={0.22} height={0.25} color="#556b2f" />
<Silk position={[1, 0.02, -34.5]} text="L1 10uH" size={0.07} opacity={0.25} />
<Capacitor position={[-4, 0, -35]} radius={0.18} height={0.35} color="#2a4a8a" />
<Silk position={[-4, 0.02, -34.5]} text="C13 100nF" size={0.07} opacity={0.25} />
<Resistor position={[3, 0, -36]} rotation={[0, 0.6, 0]} />
<Silk position={[3, 0.02, -35.5]} text="R12 330R" size={0.07} opacity={0.25} />
<Resistor position={[-1, 0, -37.5]} rotation={[0, -0.4, 0]} />
<Resistor position={[2, 0, -38]} rotation={[0, 1, 0]} />
<LED position={[0, 0, -36.5]} color="#f1c27a" />
<Silk position={[0, 0.02, -36]} text="D9 LATCH" size={0.07} opacity={0.25} />
{/* Self 2 */}
<Capacitor position={[-3, 0, -38]} radius={0.2} height={0.2} color="#556b2f" />
<Silk position={[-3, 0.02, -37.5]} text="L2 47uH" size={0.07} opacity={0.25} />
{/* ── CASES → MEDIA : FPGA Xilinx + passives ── */}
<ICComponent position={[-5, 0, -49]} size={[2.2, 0.35, 1.5]} label="U7 — XC7A35T" color="#0a0a2a" />
<Silk position={[-5, 0.02, -47]} text="FPGA Artix-7" size={0.09} opacity={0.2} />
<Capacitor position={[-2, 0, -48]} radius={0.15} height={0.3} color="#2a4a8a" />
<Silk position={[-2, 0.02, -47.5]} text="C14 100nF" size={0.07} opacity={0.25} />
<Capacitor position={[-8, 0, -48.5]} radius={0.2} height={0.4} color="#1a3a1a" />
<Silk position={[-8, 0.02, -48]} text="C15 10uF" size={0.07} opacity={0.25} />
<Resistor position={[-3, 0, -50.5]} rotation={[0, 0.5, 0]} />
<Resistor position={[-7, 0, -50]} rotation={[0, -0.7, 0]} />
<Resistor position={[-6, 0, -51]} rotation={[0, 0.3, 0]} />
<LED position={[-4, 0, -50]} color="#30d158" />
<Silk position={[-4, 0.02, -49.5]} text="D10 DONE" size={0.07} opacity={0.25} />
<LED position={[-7, 0, -49]} color="#ff6b6b" />
<Silk position={[-7, 0.02, -48.5]} text="D11 INIT" size={0.07} opacity={0.25} />
<Capacitor position={[-1, 0, -51]} radius={0.12} height={0.25} color="#2a4a8a" />
{/* ── MEDIA → SPRINTS : ARM Neural Unit + découplage ── */}
<ICComponent position={[1, 0, -59]} size={[2, 0.3, 1.2]} label="U8 — STM32N6" color="#1a0a1a" />
<Silk position={[1, 0.02, -57.5]} text="ARM Neural Unit" size={0.09} opacity={0.2} />
<Capacitor position={[4, 0, -58]} radius={0.15} height={0.3} color="#2a4a8a" />
<Silk position={[4, 0.02, -57.5]} text="C16 100nF" size={0.07} opacity={0.25} />
<Capacitor position={[-2, 0, -60]} radius={0.15} height={0.3} color="#2a4a8a" />
<Silk position={[-2, 0.02, -59.5]} text="C17 100nF" size={0.07} opacity={0.25} />
<Capacitor position={[3, 0, -61]} radius={0.2} height={0.4} color="#8B4513" />
<Silk position={[3, 0.02, -60.5]} text="C18 22uF" size={0.07} opacity={0.25} />
<Resistor position={[-1, 0, -61.5]} rotation={[0, 0.4, 0]} />
<Resistor position={[4, 0, -61]} rotation={[0, -0.6, 0]} />
<LED position={[2.5, 0, -58]} color="#5bd1d8" />
<Silk position={[2.5, 0.02, -57.5]} text="D12 NPU" size={0.07} opacity={0.25} />
{/* Self */}
<Capacitor position={[-1, 0, -58]} radius={0.18} height={0.2} color="#556b2f" />
<Silk position={[-1, 0.02, -57.5]} text="L3 4u7" size={0.07} opacity={0.25} />
{/* ── SPRINTS → CONTACT : DSP + condensateurs + LED ── */}
<ICComponent position={[5, 0, -74]} size={[1.8, 0.3, 1]} label="U9 — TMS320F" color="#1a1a0a" />
<Silk position={[5, 0.02, -72.5]} text="DSP C2000" size={0.09} opacity={0.2} />
<Capacitor position={[7.5, 0, -73]} radius={0.25} height={0.5} color="#4a2a8a" />
<Silk position={[7.5, 0.02, -72.5]} text="C19 47uF" size={0.07} opacity={0.25} />
<Capacitor position={[3, 0, -75]} radius={0.2} height={0.4} color="#8B4513" />
<Silk position={[3, 0.02, -74.5]} text="C20 10uF" size={0.07} opacity={0.25} />
<Resistor position={[6.5, 0, -75.5]} rotation={[0, -0.5, 0]} />
<Silk position={[6.5, 0.02, -75]} text="R13 1k" size={0.07} opacity={0.25} />
<Resistor position={[4, 0, -76]} rotation={[0, 0.8, 0]} />
<Silk position={[4, 0.02, -75.5]} text="R14 10k" size={0.07} opacity={0.25} />
<LED position={[2, 0, -74]} color="#ff6b35" />
<Silk position={[2, 0.02, -73.5]} text="D13 PWM" size={0.07} opacity={0.25} />
<LED position={[7, 0, -76]} color="#5bd1d8" />
<Silk position={[7, 0.02, -75.5]} text="D14 ADC" size={0.07} opacity={0.25} />
<Capacitor position={[1, 0, -77]} radius={0.2} height={0.4} color="#2a4a8a" />
<Silk position={[1, 0.02, -76.5]} text="C21 100nF" size={0.07} opacity={0.25} />
{/* Self */}
<Capacitor position={[8, 0, -76.5]} radius={0.18} height={0.2} color="#556b2f" />
<Silk position={[8, 0.02, -76]} text="L4 10uH" size={0.07} opacity={0.25} />
{/* ── Extra scattered passives ── */}
<Resistor position={[12, 0, -7]} rotation={[0, 0.7, 0]} />
<Silk position={[12, 0.02, -6.5]} text="R8" size={0.06} opacity={0.3} />
<Resistor position={[9, 0, -22]} rotation={[0, -0.4, 0]} />
<Silk position={[9, 0.02, -21.5]} text="R9" size={0.06} opacity={0.3} />
<Resistor position={[-4, 0, -24]} rotation={[0, 1.1, 0]} />
<Resistor position={[-10, -0.1, -36]} />
<Resistor position={[2, 0, -72]} rotation={[0, 0.3, 0]} />
<Capacitor position={[13, 0, -16]} radius={0.15} height={0.3} />
<Silk position={[13, 0.02, -15.5]} text="C9" size={0.06} opacity={0.3} />
<Capacitor position={[-5, 0, -60]} radius={0.15} height={0.3} />
<Silk position={[-5, 0.02, -59.5]} text="C10" size={0.06} opacity={0.3} />
{/* ── Board title silkscreen ── */}
<Silk position={[-10, 0.02, -3]} text="L'ELECTRON RARE" size={0.25} opacity={0.2} />
<Silk position={[-10, 0.02, -4]} text="REV 2026.1" size={0.1} opacity={0.15} />
<Silk position={[12, 0.02, -88]} text="Made in France" size={0.1} opacity={0.15} />
<Silk position={[12, 0.02, -89]} text="lelectronrare.fr" size={0.08} opacity={0.12} />
</group>
);
}
/* ---------- Ambient floating particles (dust/solder flux) ---------- */
function AmbientDust({ count = 200 }) {
const ref = useRef<THREE.Points>(null);
const positions = useMemo(() => {
const pos = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
pos[i * 3] = (Math.random() - 0.5) * 25;
pos[i * 3 + 1] = Math.random() * 3;
pos[i * 3 + 2] = Math.random() * -42;
}
return pos;
}, [count]);
useFrame(({ clock }) => {
if (!ref.current) return;
const arr = (ref.current.geometry.attributes.position as THREE.BufferAttribute).array as Float32Array;
const t = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
arr[i * 3 + 1] += Math.sin(t * 0.5 + i) * 0.001;
}
ref.current.geometry.attributes.position.needsUpdate = true;
});
return (
<points ref={ref}>
<bufferGeometry>
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
</bufferGeometry>
<pointsMaterial
size={0.03}
color={COLORS.current}
transparent
opacity={0.2}
sizeAttenuation
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</points>
);
}
/* ---------- Camera with zoom stops at each component ---------- */
// Define zoom stops — camera zooms in when scroll reaches each section
const ZOOM_STOPS = [
{ scroll: 0.00, pos: [0, 6, 4], look: [3, 0, -8], fov: 55 }, // Overview start
{ scroll: 0.08, pos: [10, 2.5, -10], look: [10, 0, -14], fov: 38 }, // HERO — zoom on ESP32
{ scroll: 0.22, pos: [0, 2.2, -28], look: [0, 0, -32], fov: 40 }, // ABOUT — zoom on LM358
{ scroll: 0.36, pos: [-7, 2.5, -40], look: [-8, 0, -44], fov: 38 }, // CASES — zoom on MOSFET
{ scroll: 0.48, pos: [-4, 2, -52], look: [-4, 0, -56], fov: 40 }, // MEDIA — zoom on USB-C
{ scroll: 0.62, pos: [5, 2.2, -64], look: [6, 0, -68], fov: 40 }, // SPRINTS — zoom on 7805
{ scroll: 0.78, pos: [2, 2.5, -80], look: [2, 0, -84], fov: 42 }, // CONTACT — zoom on caps
{ scroll: 1.00, pos: [0, 8, -45], look: [0, 0, -45], fov: 60 }, // End — pull out overview
];
function TraceCamera() {
const { camera } = useThree();
const smooth = useRef({ x: 0, y: 0, z: 0, lx: 0, ly: 0, lz: 0, fov: 50 });
useFrame(({ pointer }) => {
const s = scrollProgress;
// Find the two nearest zoom stops
let a = ZOOM_STOPS[0], b = ZOOM_STOPS[1];
for (let i = 0; i < ZOOM_STOPS.length - 1; i++) {
if (s >= ZOOM_STOPS[i].scroll && s <= ZOOM_STOPS[i + 1].scroll) {
a = ZOOM_STOPS[i];
b = ZOOM_STOPS[i + 1];
break;
}
}
// Interpolation factor between stops
const range = b.scroll - a.scroll;
const t = range > 0 ? (s - a.scroll) / range : 0;
// Smooth easing
const ease = t * t * (3 - 2 * t); // smoothstep
// Interpolate position
const tx = a.pos[0] + (b.pos[0] - a.pos[0]) * ease + pointer.x * 0.8;
const ty = a.pos[1] + (b.pos[1] - a.pos[1]) * ease + pointer.y * 0.3;
const tz = a.pos[2] + (b.pos[2] - a.pos[2]) * ease;
// Interpolate lookAt
const lx = a.look[0] + (b.look[0] - a.look[0]) * ease + pointer.x * 0.2;
const ly = a.look[1] + (b.look[1] - a.look[1]) * ease;
const lz = a.look[2] + (b.look[2] - a.look[2]) * ease;
// Smooth camera movement
smooth.current.x += (tx - smooth.current.x) * 0.06;
smooth.current.y += (ty - smooth.current.y) * 0.06;
smooth.current.z += (tz - smooth.current.z) * 0.06;
smooth.current.lx += (lx - smooth.current.lx) * 0.06;
smooth.current.ly += (ly - smooth.current.ly) * 0.06;
smooth.current.lz += (lz - smooth.current.lz) * 0.06;
camera.position.set(smooth.current.x, smooth.current.y, smooth.current.z);
camera.lookAt(smooth.current.lx, smooth.current.ly, smooth.current.lz);
// Smooth FOV (zoom effect)
const targetFov = a.fov + (b.fov - a.fov) * ease;
smooth.current.fov += (targetFov - smooth.current.fov) * 0.05;
(camera as THREE.PerspectiveCamera).fov = smooth.current.fov;
(camera as THREE.PerspectiveCamera).updateProjectionMatrix();
});
return null;
}
/* ---------- Full scene ---------- */
function PCBScene() {
return (
<group>
<Substrate />
<CopperTrace />
<SecondaryTraces />
<Vias />
<PCBComponents />
<CurrentFlow />
<WarmCurrentFlow />
<AmbientDust />
</group>
);
}
/* ---------- Exported component ---------- */
export default function WebGLBackground() {
useEffect(() => {
const onScroll = () => {
const el = document.documentElement;
const max = Math.max(1, el.scrollHeight - el.clientHeight);
scrollProgress = el.scrollTop / max;
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div
style={{
position: 'fixed',
inset: 0,
width: '100vw',
height: '100vh',
zIndex: 0,
pointerEvents: 'none',
}}
aria-hidden="true"
>
<Canvas
camera={{ position: [0, 3, 2], fov: 50 }}
dpr={[1, 1.5]}
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance' }}
style={{ pointerEvents: 'auto' }}
>
<color attach="background" args={['#060a06']} />
<fog attach="fog" args={['#060a06', 4, 18]} />
<ambientLight intensity={0.08} />
<pointLight position={[0, 5, 0]} intensity={0.3} color="#ffffff" distance={20} />
<directionalLight position={[5, 8, -10]} intensity={0.15} color="#5bd1d8" />
<directionalLight position={[-5, 6, -25]} intensity={0.1} color="#f1c27a" />
<TraceCamera />
<PCBScene />
<EffectComposer>
<Bloom luminanceThreshold={0.4} luminanceSmoothing={0.9} intensity={0.8} mipmapBlur />
<ChromaticAberration blendFunction={BlendFunction.NORMAL} offset={new THREE.Vector2(0.0008, 0.0008)} />
<Scanline blendFunction={BlendFunction.OVERLAY} density={2} opacity={0.04} />
</EffectComposer>
</Canvas>
</div>
);
}