From 582fee2e44ac0a836a40ab42fc5e473accc6ba14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20SAILLANT?= <108685187+electron-rare@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:50:35 +0000 Subject: [PATCH] feat: silkscreen labels + zoom camera stops on PCB components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Silkscreen text on all components (ref designators, values, section labels) - SectionLabel component: glows when camera approaches (scroll proximity) - Camera zoom stops: 8 waypoints, smooth interpolation between components - FOV transitions (50→40→42 etc.) for cinematic zoom effect - smoothstep easing between stops - Mouse parallax on camera position and lookAt - Board markings: "L'ELECTRON RARE REV 2026.1", "Made in France" Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/WebGLBackground.tsx | 234 ++++++++++++++++++++++++----- 1 file changed, 196 insertions(+), 38 deletions(-) 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;