diff --git a/src/components/WebGLBackground.tsx b/src/components/WebGLBackground.tsx
index b8560d0..ec6fb4d 100644
--- a/src/components/WebGLBackground.tsx
+++ b/src/components/WebGLBackground.tsx
@@ -1,5 +1,6 @@
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';
@@ -324,11 +325,20 @@ function ICComponent({ position, size = [2, 0.3, 1.2], label, color = '#1a1a1a'
- {/* Silkscreen label */}
-
-
-
-
+ {/* Silkscreen label on IC body */}
+
+ {label}
+
{/* Pins — both sides */}
{Array.from({ length: pins }).map((_, i) => (
@@ -435,59 +445,172 @@ function LED({ position, color = '#5bd1d8' }: { position: [number, number, numbe
);
}
+/* 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}
+
+ );
+}
+
+/* Section title — larger, glows when camera is near */
+function SectionLabel({ position, text, subtitle }: { position: [number, number, number]; text: string; subtitle?: string }) {
+ const ref = useRef(null);
+
+ useFrame(() => {
+ if (!ref.current) return;
+ const cameraZ = -scrollProgress * 38;
+ 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 (
+
+
+ {text}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ );
+}
+
/* All components assembled on the PCB */
function PCBComponents() {
return (
- {/* HERO — Main MCU (ESP32 style) */}
-
+ {/* ── HERO — Main MCU ── */}
+
+
+
+
+
+
- {/* ABOUT — Op-amp + passives */}
-
+ {/* ── ABOUT — Approche & expertise ── */}
+
+
+
+
+
+
+
- {/* CASES — Power stage (MOSFET + caps) */}
-
+ {/* ── CASES — Cas concrets ── */}
+
+
+
+
+
+
- {/* MEDIA area — Connector */}
-
+ {/* ── MEDIA — Photos & videos ── */}
+
+
+
+
- {/* SPRINTS — Voltage regulator */}
-
+ {/* ── SPRINTS — Missions ── */}
+
+
+
+
+
+
- {/* CONTACT — Big capacitor (energy storage) + indicator */}
+ {/* ── CONTACT — Energy storage ── */}
+
+
+
+
+
+
- {/* Extra scattered passives for realism */}
+ {/* ── Extra scattered passives ── */}
+
+
+
+
+
+ {/* ── Board title silkscreen ── */}
+
+
+
+
);
}
@@ -534,34 +657,69 @@ function AmbientDust({ count = 200 }) {
);
}
-/* ---------- Camera that follows the trace path on scroll ---------- */
+/* ---------- 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, 4, 2], look: [2, 0, -4], fov: 50 }, // Overview start
+ { scroll: 0.10, pos: [6, 1.8, -4], look: [6, 0, -6], fov: 40 }, // HERO — zoom on ESP32
+ { scroll: 0.25, pos: [1, 1.5, -11], look: [1, 0, -13], fov: 42 }, // ABOUT — zoom on LM358
+ { scroll: 0.40, pos: [-5, 1.8, -17], look: [-5, 0, -19], fov: 40 }, // CASES — zoom on MOSFET
+ { scroll: 0.52, pos: [-2, 1.5, -21.5], look: [-2, 0, -23], fov: 42 }, // MEDIA — zoom on USB-C
+ { scroll: 0.65, pos: [4, 1.5, -25], look: [4, 0, -27], fov: 42 }, // SPRINTS — zoom on 7805
+ { scroll: 0.80, pos: [1, 2, -33], look: [1, 0, -36], fov: 45 }, // CONTACT — zoom on caps
+ { scroll: 1.00, pos: [0, 5, -20], look: [0, 0, -20], fov: 55 }, // End — pull out overview
+];
+
function TraceCamera() {
const { camera } = useThree();
- const curve = useMemo(() => createTraceCurve(), []);
- const smooth = useRef({ progress: 0, mx: 0, my: 0 });
+ const smooth = useRef({ x: 0, y: 0, z: 0, lx: 0, ly: 0, lz: 0, fov: 50 });
useFrame(({ pointer }) => {
- // Smooth scroll interpolation
- smooth.current.progress += (scrollProgress - smooth.current.progress) * 0.06;
- smooth.current.mx += (pointer.x - smooth.current.mx) * 0.04;
- smooth.current.my += (pointer.y - smooth.current.my) * 0.04;
+ const s = scrollProgress;
- const p = Math.min(0.98, smooth.current.progress);
- const pos = curve.getPointAt(p);
- const lookAt = curve.getPointAt(Math.min(0.99, p + 0.03));
+ // 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;
+ }
+ }
- // Camera slightly above and offset by mouse
- camera.position.set(
- pos.x + smooth.current.mx * 1.5,
- pos.y + 2.5 + smooth.current.my * 0.5,
- pos.z + 1.5,
- );
+ // 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
- camera.lookAt(
- lookAt.x + smooth.current.mx * 0.3,
- lookAt.y + 0.2,
- lookAt.z,
- );
+ // 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;