refactor: clean slate — remove all WebGL, keep static + loading screen
REMOVED: - AtomGlitch.tsx (countdown 3D atom) - WebGLBackground.tsx (PCB 3D scene) - WebGLCards.tsx (scroll animations) - webgl-cards.css - All GLB models (models3d/) - Orbitron + Space Mono fonts KEPT: - static.astro (accessible version, no JS) - Loading screen with BMU photo on preview - Dark site CSS tokens in global.css - bmu-assembly.jpg photo SIMPLIFIED: - index.astro: countdown without WebGL (placeholder div ready) - preview.astro: loading screen + sections, WebGL placeholder ready - Both pages have empty #webgl-* divs for new WebGL implementation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,640 +0,0 @@
|
||||
import { useRef, useMemo, useEffect, useState, useCallback } from 'react';
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||
import { Text } from '@react-three/drei';
|
||||
import { EffectComposer, Bloom, ChromaticAberration, Scanline, Glitch as GlitchEffect } from '@react-three/postprocessing';
|
||||
import { GlitchMode, BlendFunction } from 'postprocessing';
|
||||
import * as THREE from 'three';
|
||||
|
||||
/* ===================================================================
|
||||
ATOM GLITCH — cinematic WebGL countdown for L'Electron Rare
|
||||
- Nucleus core with pulsing energy
|
||||
- 3 electron orbits with trail particles
|
||||
- Particle cloud (electrons as sparks)
|
||||
- Post-processing: bloom, chromatic aberration, glitch, scanlines
|
||||
- Mouse-reactive + auto-orbit
|
||||
=================================================================== */
|
||||
|
||||
const COLORS = {
|
||||
cyan: new THREE.Color('#5bd1d8'),
|
||||
amber: new THREE.Color('#f1c27a'),
|
||||
green: new THREE.Color('#b6d18f'),
|
||||
electric: new THREE.Color('#0071e3'),
|
||||
white: new THREE.Color('#ffffff'),
|
||||
nucleus: new THREE.Color('#ff6b35'),
|
||||
};
|
||||
|
||||
/* ---------- Particle cloud around the atom ---------- */
|
||||
function ParticleField({ count = 600 }) {
|
||||
const ref = useRef<THREE.Points>(null);
|
||||
|
||||
const { positions, velocities, colors } = useMemo(() => {
|
||||
const pos = new Float32Array(count * 3);
|
||||
const vel = new Float32Array(count * 3);
|
||||
const col = new Float32Array(count * 3);
|
||||
const palette = [COLORS.cyan, COLORS.amber, COLORS.green, COLORS.electric];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const r = 2 + Math.random() * 6;
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * Math.random() - 1);
|
||||
pos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
||||
pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||||
pos[i * 3 + 2] = r * Math.cos(phi);
|
||||
|
||||
vel[i * 3] = (Math.random() - 0.5) * 0.004;
|
||||
vel[i * 3 + 1] = (Math.random() - 0.5) * 0.004;
|
||||
vel[i * 3 + 2] = (Math.random() - 0.5) * 0.004;
|
||||
|
||||
const c = palette[Math.floor(Math.random() * palette.length)];
|
||||
col[i * 3] = c.r;
|
||||
col[i * 3 + 1] = c.g;
|
||||
col[i * 3 + 2] = c.b;
|
||||
}
|
||||
return { positions: pos, velocities: vel, colors: col };
|
||||
}, [count]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!ref.current) return;
|
||||
const posAttr = ref.current.geometry.attributes.position as THREE.BufferAttribute;
|
||||
const arr = posAttr.array as Float32Array;
|
||||
const t = clock.getElapsedTime();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
arr[i * 3] += velocities[i * 3] + Math.sin(t * 0.5 + i) * 0.002;
|
||||
arr[i * 3 + 1] += velocities[i * 3 + 1] + Math.cos(t * 0.3 + i) * 0.002;
|
||||
arr[i * 3 + 2] += velocities[i * 3 + 2];
|
||||
|
||||
// respawn far particles
|
||||
const dist = Math.sqrt(arr[i * 3] ** 2 + arr[i * 3 + 1] ** 2 + arr[i * 3 + 2] ** 2);
|
||||
if (dist > 8) {
|
||||
const r = 2 + Math.random() * 2;
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * Math.random() - 1);
|
||||
arr[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
||||
arr[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||||
arr[i * 3 + 2] = r * Math.cos(phi);
|
||||
}
|
||||
}
|
||||
posAttr.needsUpdate = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<points ref={ref}>
|
||||
<bufferGeometry>
|
||||
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
|
||||
<bufferAttribute attach="attributes-color" args={[colors, 3]} />
|
||||
</bufferGeometry>
|
||||
<pointsMaterial size={0.04} vertexColors transparent opacity={0.7} sizeAttenuation depthWrite={false} blending={THREE.AdditiveBlending} />
|
||||
</points>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Orbit trail (instanced tube of spheres) ---------- */
|
||||
function OrbitTrail({ tilt, speed, color, trailCount = 30 }: { tilt: number[]; speed: number; color: THREE.Color; trailCount?: number }) {
|
||||
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||
const phase = useMemo(() => Math.random() * Math.PI * 2, []);
|
||||
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||
const orbitA = 3.2;
|
||||
const orbitB = orbitA * 0.38;
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!meshRef.current) return;
|
||||
const t = clock.getElapsedTime() * speed + phase;
|
||||
|
||||
for (let i = 0; i < trailCount; i++) {
|
||||
const age = i / trailCount;
|
||||
const angle = t - age * 0.8;
|
||||
dummy.position.set(Math.cos(angle) * orbitA, Math.sin(angle) * orbitB, 0);
|
||||
const s = (1 - age) * 0.14 + 0.02;
|
||||
dummy.scale.setScalar(s);
|
||||
dummy.updateMatrix();
|
||||
meshRef.current.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<group rotation={tilt as [number, number, number]}>
|
||||
<instancedMesh ref={meshRef} args={[undefined, undefined, trailCount]}>
|
||||
<sphereGeometry args={[1, 8, 8]} />
|
||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={2} transparent opacity={0.9} />
|
||||
</instancedMesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Orbit ring (thin glowing line) ---------- */
|
||||
function OrbitRing({ tilt, color }: { tilt: number[]; color: THREE.Color }) {
|
||||
const points = useMemo(() => {
|
||||
const pts: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= 128; i++) {
|
||||
const t = (i / 128) * Math.PI * 2;
|
||||
pts.push(new THREE.Vector3(Math.cos(t) * 3.2, Math.sin(t) * 3.2 * 0.38, 0));
|
||||
}
|
||||
return pts;
|
||||
}, []);
|
||||
const geo = useMemo(() => new THREE.BufferGeometry().setFromPoints(points), [points]);
|
||||
|
||||
return (
|
||||
<group rotation={tilt as [number, number, number]}>
|
||||
<line geometry={geo}>
|
||||
<lineBasicMaterial color={color} transparent opacity={0.12} />
|
||||
</line>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Nucleus: pulsing energy core ---------- */
|
||||
function Nucleus() {
|
||||
const coreRef = useRef<THREE.Mesh>(null);
|
||||
const glowRef = useRef<THREE.Mesh>(null);
|
||||
const outerRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
useFrame(({ clock, pointer }) => {
|
||||
const t = clock.getElapsedTime();
|
||||
const mouseDist = Math.sqrt(pointer.x ** 2 + pointer.y ** 2);
|
||||
const mouseProximity = Math.max(0, 1 - mouseDist); // 1 at center, 0 at edges
|
||||
|
||||
if (coreRef.current) {
|
||||
const s = 1 + Math.sin(t * 3) * 0.08 + Math.sin(t * 7.3) * 0.03 + mouseProximity * 0.15;
|
||||
coreRef.current.scale.setScalar(s);
|
||||
(coreRef.current.material as THREE.MeshStandardMaterial).emissiveIntensity = 2 + mouseProximity * 3;
|
||||
}
|
||||
if (glowRef.current) {
|
||||
const s = 1 + Math.sin(t * 2) * 0.15 + mouseProximity * 0.3;
|
||||
glowRef.current.scale.setScalar(s);
|
||||
(glowRef.current.material as THREE.MeshStandardMaterial).opacity = 0.15 + Math.sin(t * 4) * 0.08 + mouseProximity * 0.2;
|
||||
}
|
||||
if (outerRef.current) {
|
||||
outerRef.current.rotation.y = t * (0.5 + mouseProximity * 2);
|
||||
outerRef.current.rotation.x = t * (0.3 + mouseProximity * 1.5);
|
||||
}
|
||||
});
|
||||
|
||||
// Logo refs — 3 copies at different rotations
|
||||
const logo1Ref = useRef<THREE.Group>(null);
|
||||
const logo2Ref = useRef<THREE.Group>(null);
|
||||
const logo3Ref = useRef<THREE.Group>(null);
|
||||
|
||||
useFrame(({ clock, camera }) => {
|
||||
const t = clock.getElapsedTime();
|
||||
// Each logo copy rotates on a different axis
|
||||
if (logo1Ref.current) {
|
||||
logo1Ref.current.rotation.y = t * 0.8;
|
||||
logo1Ref.current.quaternion.copy(camera.quaternion);
|
||||
}
|
||||
if (logo2Ref.current) {
|
||||
logo2Ref.current.rotation.set(t * 0.5, t * 0.3, 0);
|
||||
}
|
||||
if (logo3Ref.current) {
|
||||
logo3Ref.current.rotation.set(0, t * -0.4, t * 0.6);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* outer energy shell */}
|
||||
<mesh ref={outerRef}>
|
||||
<icosahedronGeometry args={[0.9, 1]} />
|
||||
<meshStandardMaterial color={COLORS.electric} wireframe transparent opacity={0.08} emissive={COLORS.electric} emissiveIntensity={0.5} />
|
||||
</mesh>
|
||||
{/* glow sphere */}
|
||||
<mesh ref={glowRef}>
|
||||
<sphereGeometry args={[0.7, 32, 32]} />
|
||||
<meshStandardMaterial color={COLORS.nucleus} transparent opacity={0.15} emissive={COLORS.nucleus} emissiveIntensity={1.2} />
|
||||
</mesh>
|
||||
{/* solid core */}
|
||||
<mesh ref={coreRef}>
|
||||
<sphereGeometry args={[0.32, 32, 32]} />
|
||||
<meshStandardMaterial color="#ffffff" emissive={COLORS.amber} emissiveIntensity={2} metalness={0.6} roughness={0.2} />
|
||||
</mesh>
|
||||
|
||||
{/* ER logo — 3 copies at center, different rotations */}
|
||||
<group ref={logo1Ref}>
|
||||
<Text fontSize={0.25} font={FONT_URL} anchorX="center" anchorY="middle" letterSpacing={0.05}>
|
||||
ER
|
||||
<meshStandardMaterial color="#ffffff" emissive="#5bd1d8" emissiveIntensity={1.5} transparent opacity={0.7} />
|
||||
</Text>
|
||||
</group>
|
||||
<group ref={logo2Ref}>
|
||||
<Text fontSize={0.2} font={FONT_URL} anchorX="center" anchorY="middle" letterSpacing={0.05}>
|
||||
ER
|
||||
<meshStandardMaterial color="#ffffff" emissive="#f1c27a" emissiveIntensity={1} transparent opacity={0.35} />
|
||||
</Text>
|
||||
</group>
|
||||
<group ref={logo3Ref}>
|
||||
<Text fontSize={0.18} font={FONT_URL} anchorX="center" anchorY="middle" letterSpacing={0.05}>
|
||||
ER
|
||||
<meshStandardMaterial color="#ffffff" emissive="#b6d18f" emissiveIntensity={0.8} transparent opacity={0.25} />
|
||||
</Text>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- ER logo (PNG) orbiting close to nucleus ---------- */
|
||||
function LogoOrbit({ tilt, speed, radius, color }: {
|
||||
tilt: number[]; speed: number; radius: number; color: string;
|
||||
}) {
|
||||
const ref = useRef<THREE.Group>(null);
|
||||
const planeRef = useRef<THREE.Group>(null);
|
||||
const [texture, setTexture] = useState<THREE.Texture | null>(null);
|
||||
const phase = useMemo(() => Math.random() * Math.PI * 2, []);
|
||||
const eccentricity = 0.35;
|
||||
const threeColor = useMemo(() => new THREE.Color(color), [color]);
|
||||
|
||||
useEffect(() => {
|
||||
new THREE.TextureLoader().load('/assets/brand/logo-mark.png', (tex) => {
|
||||
tex.minFilter = THREE.LinearFilter;
|
||||
tex.magFilter = THREE.LinearFilter;
|
||||
setTexture(tex);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useFrame(({ clock, camera }) => {
|
||||
if (!ref.current || !planeRef.current) return;
|
||||
const t = clock.getElapsedTime() * speed + phase;
|
||||
|
||||
ref.current.position.set(
|
||||
Math.cos(t) * radius,
|
||||
Math.sin(t) * radius * eccentricity,
|
||||
0,
|
||||
);
|
||||
|
||||
// Billboard — always face camera
|
||||
planeRef.current.quaternion.copy(camera.quaternion);
|
||||
});
|
||||
|
||||
if (!texture) return null;
|
||||
|
||||
return (
|
||||
<group rotation={tilt as [number, number, number]}>
|
||||
<group ref={ref}>
|
||||
<group ref={planeRef}>
|
||||
{/* Logo PNG on a plane */}
|
||||
<mesh>
|
||||
<planeGeometry args={[0.35, 0.35]} />
|
||||
<meshStandardMaterial
|
||||
map={texture}
|
||||
transparent
|
||||
opacity={0.85}
|
||||
emissive={threeColor}
|
||||
emissiveIntensity={0.8}
|
||||
side={THREE.DoubleSide}
|
||||
alphaTest={0.1}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
{/* Glow sphere behind logo */}
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.15, 8, 8]} />
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={threeColor}
|
||||
emissiveIntensity={1.2}
|
||||
transparent
|
||||
opacity={0.15}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Energy arcs (random lightning) ---------- */
|
||||
function EnergyArc({ color }: { color: THREE.Color }) {
|
||||
const ref = useRef<THREE.Line>(null);
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
const t = clock.getElapsedTime();
|
||||
// flash randomly
|
||||
setVisible(Math.sin(t * 12 + Math.random() * 100) > 0.7);
|
||||
|
||||
if (ref.current && visible) {
|
||||
const geo = ref.current.geometry;
|
||||
const pts: THREE.Vector3[] = [];
|
||||
const segments = 8;
|
||||
const startAngle = Math.sin(t * 2) * Math.PI;
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const frac = i / segments;
|
||||
const r = 0.4 + frac * 2.5;
|
||||
const angle = startAngle + frac * 1.5;
|
||||
pts.push(new THREE.Vector3(
|
||||
Math.cos(angle) * r + (Math.random() - 0.5) * 0.3,
|
||||
Math.sin(angle) * r * 0.4 + (Math.random() - 0.5) * 0.2,
|
||||
(Math.random() - 0.5) * 0.4,
|
||||
));
|
||||
}
|
||||
geo.setFromPoints(pts);
|
||||
}
|
||||
});
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<line ref={ref}>
|
||||
<bufferGeometry />
|
||||
<lineBasicMaterial color={color} transparent opacity={0.4} blending={THREE.AdditiveBlending} />
|
||||
</line>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 3D Circular / Orbital text ---------- */
|
||||
const LAUNCH = new Date('2026-05-01T00:00:00+02:00').getTime();
|
||||
const FONT_URL = '/assets/fonts/orbitron-bold.ttf';
|
||||
|
||||
function pad(n: number) { return String(n).padStart(2, '0'); }
|
||||
|
||||
/* Each character placed on a circle arc, rotating together */
|
||||
function CircularText({ text, radius, speed, tilt, fontSize, color, emissive, opacity = 0.9, spread = 0.8 }: {
|
||||
text: string; radius: number; speed: number; tilt: number[];
|
||||
fontSize: number; color: string; emissive: string; opacity?: number; spread?: number;
|
||||
}) {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const chars = useMemo(() => text.split(''), [text]);
|
||||
const charWidth = fontSize * 0.55;
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!groupRef.current) return;
|
||||
const t = clock.getElapsedTime() * speed;
|
||||
groupRef.current.rotation.y = t;
|
||||
});
|
||||
|
||||
return (
|
||||
<group rotation={tilt as [number, number, number]}>
|
||||
<group ref={groupRef}>
|
||||
{chars.map((char, i) => {
|
||||
const angle = ((i - chars.length / 2) * charWidth * spread) / radius;
|
||||
const x = Math.sin(angle) * radius;
|
||||
const z = Math.cos(angle) * radius;
|
||||
return (
|
||||
<Text
|
||||
key={i}
|
||||
position={[x, 0, z]}
|
||||
rotation={[0, -angle, 0]}
|
||||
fontSize={fontSize}
|
||||
font={FONT_URL}
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
{char}
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={emissive}
|
||||
emissiveIntensity={0.6}
|
||||
transparent
|
||||
opacity={opacity}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* Countdown — characters on a spinning ring */
|
||||
function CountdownRing() {
|
||||
const [time, setTime] = useState('');
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const radius = 2.5;
|
||||
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
const diff = Math.max(0, LAUNCH - Date.now());
|
||||
const d = Math.floor(diff / 86400000);
|
||||
const h = Math.floor((diff % 86400000) / 3600000);
|
||||
const m = Math.floor((diff % 3600000) / 60000);
|
||||
const s = Math.floor((diff % 60000) / 1000);
|
||||
setTime(`${pad(d)}J ${pad(h)}H ${pad(m)}M ${pad(s)}S`);
|
||||
}
|
||||
tick();
|
||||
const id = setInterval(tick, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!groupRef.current) return;
|
||||
groupRef.current.rotation.y = clock.getElapsedTime() * -0.2;
|
||||
});
|
||||
|
||||
const chars = time.split('');
|
||||
const fontSize = 0.28;
|
||||
const charWidth = fontSize * 0.5;
|
||||
|
||||
return (
|
||||
<group rotation={[0.3, 0, 0.1]}>
|
||||
<group ref={groupRef}>
|
||||
{chars.map((char, i) => {
|
||||
const angle = ((i - chars.length / 2) * charWidth * 0.9) / radius;
|
||||
const x = Math.sin(angle) * radius;
|
||||
const z = Math.cos(angle) * radius;
|
||||
return (
|
||||
<Text
|
||||
key={i}
|
||||
position={[x, 0, z]}
|
||||
rotation={[0, -angle, 0]}
|
||||
fontSize={fontSize}
|
||||
font={FONT_URL}
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
{char}
|
||||
<meshStandardMaterial
|
||||
color="#ffffff"
|
||||
emissive="#5bd1d8"
|
||||
emissiveIntensity={0.8}
|
||||
transparent
|
||||
opacity={0.95}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* All circular texts assembled */
|
||||
function CountdownText() {
|
||||
return (
|
||||
<group>
|
||||
{/* "L'electron" — tight elliptical orbit, close to nucleus */}
|
||||
<CircularText
|
||||
text={"L'\u00e9lectron"}
|
||||
radius={2.0}
|
||||
speed={0.18}
|
||||
tilt={[-0.4, 0, 0]}
|
||||
fontSize={0.45}
|
||||
color="#ffffff"
|
||||
emissive="#ffffff"
|
||||
spread={1.2}
|
||||
/>
|
||||
|
||||
{/* "rare" — opposite tilt, close */}
|
||||
<CircularText
|
||||
text="rare"
|
||||
radius={1.8}
|
||||
speed={-0.22}
|
||||
tilt={[0.6, 0.3, 0]}
|
||||
fontSize={0.7}
|
||||
color="#ffffff"
|
||||
emissive="#5bd1d8"
|
||||
spread={1.3}
|
||||
/>
|
||||
|
||||
{/* Subtitle — medium ring */}
|
||||
<CircularText
|
||||
text="SYSTEMES ELECTRONIQUES SPECIFIQUES"
|
||||
radius={3.0}
|
||||
speed={0.1}
|
||||
tilt={[1.2, 0.2, 0.3]}
|
||||
fontSize={0.1}
|
||||
color="#ffffff"
|
||||
emissive="#5bd1d8"
|
||||
opacity={0.35}
|
||||
spread={0.6}
|
||||
/>
|
||||
|
||||
{/* "LANCEMENT DANS" — small tight ring */}
|
||||
<CircularText
|
||||
text="LANCEMENT DANS"
|
||||
radius={1.5}
|
||||
speed={0.3}
|
||||
tilt={[-0.8, -0.2, 0.15]}
|
||||
fontSize={0.09}
|
||||
color="#5bd1d8"
|
||||
emissive="#5bd1d8"
|
||||
opacity={0.5}
|
||||
spread={0.8}
|
||||
/>
|
||||
|
||||
{/* 3x ER logo orbiting close to nucleus like inner electrons */}
|
||||
<LogoOrbit tilt={[-0.3, 0, 0.1]} speed={0.6} radius={1.2} color="#5bd1d8" />
|
||||
<LogoOrbit tilt={[0.5, 0.2, -0.1]} speed={-0.45} radius={1.0} color="#f1c27a" />
|
||||
<LogoOrbit tilt={[1.2, -0.3, 0.3]} speed={0.35} radius={1.3} color="#b6d18f" />
|
||||
|
||||
{/* Countdown timer — spinning ring */}
|
||||
<CountdownRing />
|
||||
|
||||
{/* Extra decorative text rings */}
|
||||
<CircularText
|
||||
text="electronique automatisme energie stockage prototypage formation"
|
||||
radius={3.5}
|
||||
speed={-0.06}
|
||||
tilt={[0.8, -0.4, 0.5]}
|
||||
fontSize={0.06}
|
||||
color="#5bd1d8"
|
||||
emissive="#5bd1d8"
|
||||
opacity={0.2}
|
||||
spread={0.5}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Main scene ---------- */
|
||||
function AtomScene() {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
|
||||
const orbits = useMemo(() => [
|
||||
{ tilt: [-0.5, 0, 0], speed: 1.0, color: COLORS.cyan },
|
||||
{ tilt: [0.5, 0.3, 0], speed: 0.72, color: COLORS.amber },
|
||||
{ tilt: [1.57, 0.2, 0.4], speed: 0.55, color: COLORS.green },
|
||||
], []);
|
||||
|
||||
useFrame(({ clock, pointer }) => {
|
||||
if (!groupRef.current) return;
|
||||
const t = clock.getElapsedTime();
|
||||
// Mouse distance from center = speed boost
|
||||
const mouseDist = Math.sqrt(pointer.x ** 2 + pointer.y ** 2);
|
||||
const speedMul = 1 + mouseDist * 0.8;
|
||||
groupRef.current.rotation.y = t * 0.12 * speedMul + pointer.x * 0.6;
|
||||
groupRef.current.rotation.x = -0.2 + pointer.y * 0.4;
|
||||
groupRef.current.rotation.z = pointer.x * 0.1;
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
<Nucleus />
|
||||
{orbits.map((o, i) => (
|
||||
<OrbitRing key={`ring-${i}`} tilt={o.tilt} color={o.color} />
|
||||
))}
|
||||
{orbits.map((o, i) => (
|
||||
<OrbitTrail key={`trail-${i}`} tilt={o.tilt} speed={o.speed} color={o.color} />
|
||||
))}
|
||||
<EnergyArc color={COLORS.cyan} />
|
||||
<EnergyArc color={COLORS.amber} />
|
||||
<ParticleField />
|
||||
{/* Textes orbitants — dans le groupe, tournent avec l'atome */}
|
||||
<CountdownText />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Camera with mouse parallax ---------- */
|
||||
function CameraRig() {
|
||||
const { camera } = useThree();
|
||||
const mouse = useRef({ x: 0, y: 0 });
|
||||
const smooth = useRef({ x: 0, y: 0 });
|
||||
|
||||
useFrame(({ clock, pointer }) => {
|
||||
const t = clock.getElapsedTime();
|
||||
|
||||
// Smooth mouse follow (lerp)
|
||||
mouse.current.x = pointer.x;
|
||||
mouse.current.y = pointer.y;
|
||||
smooth.current.x += (mouse.current.x - smooth.current.x) * 0.05;
|
||||
smooth.current.y += (mouse.current.y - smooth.current.y) * 0.05;
|
||||
|
||||
// Camera orbits gently + follows mouse
|
||||
camera.position.x = Math.sin(t * 0.08) * 0.5 + smooth.current.x * 2.5;
|
||||
camera.position.y = Math.cos(t * 0.06) * 0.3 + smooth.current.y * 1.5;
|
||||
camera.position.z = 9 + Math.sin(t * 0.1) * 0.3;
|
||||
camera.lookAt(smooth.current.x * 0.5, smooth.current.y * 0.3, 0);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- Exported component ---------- */
|
||||
export default function AtomGlitch() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
aria-label="Atome 3D animé — L'Electron Rare"
|
||||
role="img"
|
||||
>
|
||||
<Canvas
|
||||
camera={{ position: [0, -0.5, 9], fov: 45 }}
|
||||
dpr={[1, 2]}
|
||||
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance' }}
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
<color attach="background" args={['#000000']} />
|
||||
<fog attach="fog" args={['#000000', 8, 18]} />
|
||||
|
||||
<ambientLight intensity={0.15} />
|
||||
<pointLight position={[5, 3, 5]} intensity={0.8} color="#ffffff" />
|
||||
<pointLight position={[-4, -2, 3]} intensity={0.4} color="#5bd1d8" />
|
||||
<pointLight position={[0, 4, -3]} intensity={0.3} color="#f1c27a" />
|
||||
|
||||
<CameraRig />
|
||||
<AtomScene />
|
||||
|
||||
<EffectComposer>
|
||||
<Bloom luminanceThreshold={0.2} luminanceSmoothing={0.9} intensity={1.8} mipmapBlur />
|
||||
<ChromaticAberration blendFunction={BlendFunction.NORMAL} offset={new THREE.Vector2(0.002, 0.002)} />
|
||||
<GlitchEffect delay={new THREE.Vector2(3, 8)} duration={new THREE.Vector2(0.1, 0.4)} strength={new THREE.Vector2(0.05, 0.15)} mode={GlitchMode.SPORADIC} />
|
||||
<Scanline blendFunction={BlendFunction.OVERLAY} density={1.8} opacity={0.05} />
|
||||
</EffectComposer>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
import { useRef, useMemo, useEffect, useState, Suspense } from 'react';
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||
import { Text, useGLTF } from '@react-three/drei';
|
||||
import { EffectComposer, Bloom, ChromaticAberration, Scanline } from '@react-three/postprocessing';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
import * as THREE from 'three';
|
||||
|
||||
/* ===================================================================
|
||||
PCB JOURNEY v2 — Real KiCad PCB + real component GLBs
|
||||
|
||||
The BMU v2 PCB is loaded as the central 3D object.
|
||||
Camera orbits around it, zooming into component zones on scroll.
|
||||
Real GLB components from KiCad library placed on the board.
|
||||
Current flows along the copper traces.
|
||||
=================================================================== */
|
||||
|
||||
const COLORS = {
|
||||
copper: new THREE.Color('#c87533'),
|
||||
copperBright: new THREE.Color('#e8a040'),
|
||||
current: new THREE.Color('#5bd1d8'),
|
||||
currentWarm: new THREE.Color('#f1c27a'),
|
||||
silkscreen: new THREE.Color('#e8e8d0'),
|
||||
};
|
||||
|
||||
const FONT_URL = '/assets/fonts/orbitron-bold.ttf';
|
||||
|
||||
/* ---------- Scroll state ---------- */
|
||||
let scrollProgress = 0;
|
||||
|
||||
/* ---------- Real PCB Board (full 3D with components from FreeCAD) ---------- */
|
||||
function RealPCB() {
|
||||
let glb: any = null;
|
||||
try { glb = useGLTF('/assets/models3d/bmu-assembly.glb'); } catch {}
|
||||
|
||||
if (!glb?.scene) return null;
|
||||
|
||||
// Deep clone with materials preserved
|
||||
const cloned = useMemo(() => {
|
||||
const clone = glb.scene.clone(true);
|
||||
clone.traverse((child: any) => {
|
||||
if (child.isMesh && child.material) {
|
||||
child.material = child.material.clone();
|
||||
}
|
||||
});
|
||||
return clone;
|
||||
}, [glb.scene]);
|
||||
|
||||
return (
|
||||
<primitive
|
||||
object={cloned}
|
||||
scale={1}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
position={[0, 0, 0]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* ---------- GLB Component placement ---------- */
|
||||
function GLBComponent({ modelUrl, position, scale = 1, rotation = [0, 0, 0] as [number, number, number] }: {
|
||||
modelUrl: string; position: [number, number, number]; scale?: number; rotation?: [number, number, number];
|
||||
}) {
|
||||
let glb: any = null;
|
||||
try { glb = useGLTF(modelUrl); } catch {}
|
||||
if (!glb?.scene) return null;
|
||||
return (
|
||||
<primitive
|
||||
object={glb.scene.clone()}
|
||||
position={position}
|
||||
scale={scale}
|
||||
rotation={rotation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Section label (silkscreen on board, clickable) ---------- */
|
||||
function SectionLabel({ position, text, subtitle, scrollTarget }: {
|
||||
position: [number, number, number]; text: string; subtitle?: string; scrollTarget?: string;
|
||||
}) {
|
||||
const ref = useRef<THREE.Group>(null);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
useFrame(({ camera }) => {
|
||||
if (!ref.current) return;
|
||||
const worldPos = new THREE.Vector3();
|
||||
ref.current.getWorldPosition(worldPos);
|
||||
const dist = camera.position.distanceTo(worldPos);
|
||||
const glow = Math.max(0, 1 - dist / 12);
|
||||
// Dynamic opacity based on camera distance
|
||||
ref.current.children.forEach(child => {
|
||||
if ((child as any).material) {
|
||||
(child as any).material.opacity = hovered ? 0.9 : 0.3 + glow * 0.4;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
if (!scrollTarget) return;
|
||||
if (scrollTarget === '#contact') {
|
||||
// Open contact popup
|
||||
document.getElementById('contact')?.classList.add('contact-3d-popup--open');
|
||||
} else {
|
||||
const el = document.querySelector(scrollTarget);
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<group
|
||||
ref={ref}
|
||||
position={position}
|
||||
onClick={handleClick}
|
||||
onPointerOver={() => { setHovered(true); document.body.style.cursor = scrollTarget ? 'pointer' : 'default'; }}
|
||||
onPointerOut={() => { setHovered(false); document.body.style.cursor = 'default'; }}
|
||||
>
|
||||
{/* Clickable hit area (invisible plane) */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.01, 0.4]}>
|
||||
<planeGeometry args={[text.length * 0.55, 2.5]} />
|
||||
<meshBasicMaterial transparent opacity={0} side={THREE.DoubleSide} />
|
||||
</mesh>
|
||||
|
||||
{/* Main label */}
|
||||
<Text
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
fontSize={hovered ? 0.9 : 0.8}
|
||||
font={FONT_URL}
|
||||
color={hovered ? '#ffffff' : '#5bd1d8'}
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
material-transparent
|
||||
material-opacity={0.4}
|
||||
letterSpacing={0.08}
|
||||
>
|
||||
{text}
|
||||
<meshStandardMaterial
|
||||
color={hovered ? '#ffffff' : '#5bd1d8'}
|
||||
emissive={hovered ? '#5bd1d8' : '#5bd1d8'}
|
||||
emissiveIntensity={hovered ? 1.5 : 0.3}
|
||||
transparent
|
||||
opacity={0.4}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
{/* Subtitle */}
|
||||
{subtitle && (
|
||||
<Text
|
||||
position={[0, 0, 1.2]}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
fontSize={0.3}
|
||||
font={FONT_URL}
|
||||
color="#e8e8d0"
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
material-transparent
|
||||
material-opacity={hovered ? 0.5 : 0.2}
|
||||
letterSpacing={0.04}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Hover glow ring */}
|
||||
{hovered && (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0.4]}>
|
||||
<ringGeometry args={[text.length * 0.25, text.length * 0.28, 32]} />
|
||||
<meshStandardMaterial color="#5bd1d8" emissive="#5bd1d8" emissiveIntensity={2} transparent opacity={0.15} side={THREE.DoubleSide} />
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Current flow particles ---------- */
|
||||
function CurrentFlow({ count = 80 }) {
|
||||
const ref = useRef<THREE.InstancedMesh>(null);
|
||||
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||
|
||||
// Particles follow a circular path around the PCB
|
||||
const phases = useMemo(() => Array.from({ length: count }, () => Math.random()), [count]);
|
||||
const radii = useMemo(() => Array.from({ length: count }, () => 3 + Math.random() * 8), [count]);
|
||||
const heights = useMemo(() => Array.from({ length: count }, () => 0.3 + Math.random() * 0.5), [count]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!ref.current) return;
|
||||
const t = clock.getElapsedTime();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = phases[i] * Math.PI * 2 + t * (0.1 + phases[i] * 0.1);
|
||||
const r = radii[i];
|
||||
dummy.position.set(
|
||||
Math.cos(angle) * r,
|
||||
heights[i] + Math.sin(t * 2 + i) * 0.1,
|
||||
Math.sin(angle) * r * 0.6,
|
||||
);
|
||||
const proximity = Math.max(0.2, 1 - Math.abs(scrollProgress - phases[i]) * 3);
|
||||
dummy.scale.setScalar(0.04 * proximity);
|
||||
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.8} />
|
||||
</instancedMesh>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Warm current (second layer) ---------- */
|
||||
function WarmCurrent({ count = 40 }) {
|
||||
const ref = useRef<THREE.InstancedMesh>(null);
|
||||
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||
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 angle = phases[i] * Math.PI * 2 + t * 0.06 + Math.PI;
|
||||
const r = 4 + phases[i] * 6;
|
||||
dummy.position.set(
|
||||
Math.cos(angle) * r,
|
||||
0.2 + Math.sin(t + i) * 0.05,
|
||||
Math.sin(angle) * r * 0.5,
|
||||
);
|
||||
dummy.scale.setScalar(0.025);
|
||||
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.6} />
|
||||
</instancedMesh>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Ambient dust ---------- */
|
||||
function AmbientDust({ count = 150 }) {
|
||||
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) * 30;
|
||||
pos[i * 3 + 1] = Math.random() * 4;
|
||||
pos[i * 3 + 2] = (Math.random() - 0.5) * 20;
|
||||
}
|
||||
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.3 + i * 0.5) * 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.04} color={COLORS.current} transparent opacity={0.15} sizeAttenuation depthWrite={false} blending={THREE.AdditiveBlending} />
|
||||
</points>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Component clusters around the PCB ---------- */
|
||||
function ComponentCluster() {
|
||||
const R = [-Math.PI / 2, 0, 0] as [number, number, number];
|
||||
const S_RES = 8;
|
||||
const S_CAP = 10;
|
||||
const S_LED = 8;
|
||||
const S_IC_SM = 6;
|
||||
const S_IC_LG = 3;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Zone 1 — MCU area (top-right of board) */}
|
||||
<SectionLabel position={[40, 90, -10]} text="MCU" subtitle="Approche & expertise" scrollTarget="#a-propos" />
|
||||
<GLBComponent modelUrl="/assets/models3d/qfp32.glb" position={[5, 0.3, -2]} scale={S_IC_LG} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/soic8.glb" position={[3, 0.3, -1]} scale={S_IC_SM} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[4, 0.3, 0]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[4.5, 0.3, 0.5]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[6.5, 0.3, -0.5]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[7, 0.3, -3]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/led_0603.glb" position={[7.5, 0.3, -1]} scale={S_LED} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/led_0603.glb" position={[7.5, 0.3, -2.5]} scale={S_LED} rotation={R} />
|
||||
|
||||
{/* Zone 2 — Analog / Op-amp area (left of board) */}
|
||||
<SectionLabel position={[-30, 90, -10]} text="ANALOG" subtitle="Cas concrets" scrollTarget="#cas-concrets" />
|
||||
<GLBComponent modelUrl="/assets/models3d/soic8.glb" position={[-5, 0.3, -1]} scale={S_IC_SM} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/soic8.glb" position={[-3, 0.3, -2]} scale={S_IC_SM} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[-4, 0.3, 0]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[-6, 0.3, -2]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[-4.5, 0.3, -3]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[-6.5, 0.3, 0]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/inductor_0805.glb" position={[-3.5, 0.3, 1]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/led_0603.glb" position={[-2, 0.3, -1]} scale={S_LED} rotation={R} />
|
||||
|
||||
{/* Zone 3 — Power stage (bottom-right) */}
|
||||
<SectionLabel position={[40, 30, -10]} text="POWER" subtitle="Photos & videos terrain" scrollTarget="#photos" />
|
||||
<GLBComponent modelUrl="/assets/models3d/qfp32.glb" position={[4, 0.3, 4]} scale={S_IC_LG} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[2, 0.3, 3]} scale={S_CAP * 1.5} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[6, 0.3, 5]} scale={S_CAP * 1.5} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[3, 0.3, 5.5]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/inductor_0805.glb" position={[5, 0.3, 3]} scale={S_CAP * 1.2} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[5.5, 0.3, 6]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/led_0603.glb" position={[7, 0.3, 4.5]} scale={S_LED} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/led_0603.glb" position={[1.5, 0.3, 4.5]} scale={S_LED} rotation={R} />
|
||||
|
||||
{/* Zone 4 — Communication (bottom-left) */}
|
||||
<SectionLabel position={[-30, 30, -10]} text="FORMATION" subtitle="Workshops & programmes" scrollTarget="/formation/" />
|
||||
<GLBComponent modelUrl="/assets/models3d/soic8.glb" position={[-5, 0.3, 4]} scale={S_IC_SM} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/soic8.glb" position={[-3, 0.3, 5]} scale={S_IC_SM} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[-6, 0.3, 3]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[-4, 0.3, 6]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[-6.5, 0.3, 5]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/led_0603.glb" position={[-2, 0.3, 3.5]} scale={S_LED} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/led_0603.glb" position={[-7, 0.3, 4.5]} scale={S_LED} rotation={R} />
|
||||
|
||||
{/* Zone 5 — DSP / Neural (center-top) */}
|
||||
<SectionLabel position={[4, 110, -10]} text="MISSIONS" subtitle="Diagnostic · Prototype · Production" scrollTarget="#graphic-sprints-title" />
|
||||
<GLBComponent modelUrl="/assets/models3d/qfp32.glb" position={[0, 0.3, -4]} scale={S_IC_LG} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[-2, 0.3, -4.5]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/capacitor_0805.glb" position={[2, 0.3, -3.5]} scale={S_CAP} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[-1, 0.3, -5]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/resistor_0603.glb" position={[1, 0.3, -5.5]} scale={S_RES} rotation={R} />
|
||||
<GLBComponent modelUrl="/assets/models3d/inductor_0805.glb" position={[0, 0.3, -6]} scale={S_CAP} rotation={R} />
|
||||
|
||||
{/* Zone 6 — Contact area (center) */}
|
||||
<SectionLabel position={[4, 66, -10]} text="CONTACT" subtitle="Discuter de votre projet" scrollTarget="#contact" />
|
||||
|
||||
{/* Scattered passives */}
|
||||
{Array.from({ length: 15 }).map((_, i) => (
|
||||
<GLBComponent
|
||||
key={`scat-r-${i}`}
|
||||
modelUrl="/assets/models3d/resistor_0603.glb"
|
||||
position={[(Math.random() - 0.5) * 16, 0.3, (Math.random() - 0.5) * 14]}
|
||||
scale={S_RES}
|
||||
rotation={[-Math.PI / 2, 0, Math.random() * Math.PI]}
|
||||
/>
|
||||
))}
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<GLBComponent
|
||||
key={`scat-c-${i}`}
|
||||
modelUrl="/assets/models3d/capacitor_0805.glb"
|
||||
position={[(Math.random() - 0.5) * 16, 0.3, (Math.random() - 0.5) * 14]}
|
||||
scale={S_CAP}
|
||||
rotation={R}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Board title silkscreen ---------- */
|
||||
/* ---------- Light that follows the camera ---------- */
|
||||
function CameraLight() {
|
||||
const lightRef = useRef<THREE.PointLight>(null);
|
||||
const { camera } = useThree();
|
||||
|
||||
useFrame(() => {
|
||||
if (!lightRef.current) return;
|
||||
lightRef.current.position.copy(camera.position);
|
||||
lightRef.current.position.y += 0.03;
|
||||
});
|
||||
|
||||
return (
|
||||
<pointLight ref={lightRef} intensity={0.6} color="#ffffff" distance={0.5} decay={1.5} />
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Camera — orbits around PCB, zooms on scroll ---------- */
|
||||
// STEP units = METERS. Board ~0.17m wide, ~0.10m tall. After -π/2 X rotation:
|
||||
// model Y→scene -Z, model Z→scene Y. Board center ≈ (0, 0.04, 0).
|
||||
const CAMERA_STOPS = [
|
||||
{ scroll: 0.00, pos: [0, 0.3, 0.2], look: [0, 0.04, 0] }, // Overview
|
||||
{ scroll: 0.12, pos: [0.06, 0.12, 0.02], look: [0.04, 0.04, 0] }, // MCU (right)
|
||||
{ scroll: 0.28, pos: [-0.06, 0.12, 0.02], look: [-0.04, 0.04, 0] }, // Analog (left)
|
||||
{ scroll: 0.42, pos: [0.05, 0.08, -0.03], look: [0.03, 0.04, -0.02] }, // Power
|
||||
{ scroll: 0.56, pos: [-0.05, 0.08, -0.03], look: [-0.03, 0.04, -0.02] }, // Formation
|
||||
{ scroll: 0.70, pos: [0, 0.15, 0.05], look: [0, 0.06, 0] }, // Missions
|
||||
{ scroll: 0.85, pos: [0, 0.08, 0.01], look: [0, 0.04, 0] }, // Contact (close)
|
||||
{ scroll: 1.00, pos: [0, 0.4, 0.25], look: [0, 0.04, 0] }, // Pull back
|
||||
];
|
||||
|
||||
function OrbitCamera() {
|
||||
const { camera } = useThree();
|
||||
const smooth = useRef({ x: 0, y: 0, z: 0, lx: 0, ly: 0, lz: 0 });
|
||||
|
||||
useFrame(({ pointer }) => {
|
||||
const s = scrollProgress;
|
||||
|
||||
let a = CAMERA_STOPS[0], b = CAMERA_STOPS[1];
|
||||
for (let i = 0; i < CAMERA_STOPS.length - 1; i++) {
|
||||
if (s >= CAMERA_STOPS[i].scroll && s <= CAMERA_STOPS[i + 1].scroll) {
|
||||
a = CAMERA_STOPS[i];
|
||||
b = CAMERA_STOPS[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const range = b.scroll - a.scroll;
|
||||
const t = range > 0 ? (s - a.scroll) / range : 0;
|
||||
const ease = t * t * (3 - 2 * t);
|
||||
|
||||
const tx = a.pos[0] + (b.pos[0] - a.pos[0]) * ease + pointer.x * 0.03;
|
||||
const ty = a.pos[1] + (b.pos[1] - a.pos[1]) * ease + pointer.y * 0.01;
|
||||
const tz = a.pos[2] + (b.pos[2] - a.pos[2]) * ease;
|
||||
const lx = a.look[0] + (b.look[0] - a.look[0]) * ease + pointer.x * 0.005;
|
||||
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.current.x += (tx - smooth.current.x) * 0.05;
|
||||
smooth.current.y += (ty - smooth.current.y) * 0.05;
|
||||
smooth.current.z += (tz - smooth.current.z) * 0.05;
|
||||
smooth.current.lx += (lx - smooth.current.lx) * 0.05;
|
||||
smooth.current.ly += (ly - smooth.current.ly) * 0.05;
|
||||
smooth.current.lz += (lz - smooth.current.lz) * 0.05;
|
||||
|
||||
camera.position.set(smooth.current.x, smooth.current.y, smooth.current.z);
|
||||
camera.lookAt(smooth.current.lx, smooth.current.ly, smooth.current.lz);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- Full scene ---------- */
|
||||
function PCBScene() {
|
||||
return (
|
||||
<group>
|
||||
<Suspense fallback={null}>
|
||||
<RealPCB />
|
||||
<ComponentCluster />
|
||||
</Suspense>
|
||||
<CurrentFlow />
|
||||
<WarmCurrent />
|
||||
<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, 0.3, 0.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', 0.2, 0.8]} />
|
||||
|
||||
<ambientLight intensity={0.25} />
|
||||
<directionalLight position={[0.1, 0.2, 0.1]} intensity={0.7} color="#ffffff" />
|
||||
<directionalLight position={[-0.1, 0.15, -0.1]} intensity={0.3} color="#5bd1d8" />
|
||||
<pointLight position={[0, 0.1, 0]} intensity={0.4} color="#f1c27a" distance={0.5} />
|
||||
<CameraLight />
|
||||
|
||||
<OrbitCamera />
|
||||
<PCBScene />
|
||||
|
||||
<EffectComposer>
|
||||
<Bloom luminanceThreshold={0.4} luminanceSmoothing={0.9} intensity={0.7} mipmapBlur />
|
||||
<ChromaticAberration blendFunction={BlendFunction.NORMAL} offset={new THREE.Vector2(0.0006, 0.0006)} />
|
||||
<Scanline blendFunction={BlendFunction.OVERLAY} density={1.5} opacity={0.03} />
|
||||
</EffectComposer>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Preload all models */
|
||||
useGLTF.preload('/assets/models3d/bmu-assembly.glb');
|
||||
useGLTF.preload('/assets/models3d/resistor_0603.glb');
|
||||
useGLTF.preload('/assets/models3d/capacitor_0805.glb');
|
||||
useGLTF.preload('/assets/models3d/inductor_0805.glb');
|
||||
useGLTF.preload('/assets/models3d/led_0603.glb');
|
||||
useGLTF.preload('/assets/models3d/soic8.glb');
|
||||
useGLTF.preload('/assets/models3d/qfp32.glb');
|
||||
@@ -1,203 +0,0 @@
|
||||
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
/* ===================================================================
|
||||
WEBGL CARDS — Scroll-triggered tile animations + media lightbox
|
||||
|
||||
Features:
|
||||
- Cards materialize with particle burst on scroll-in
|
||||
- Cards dissolve with particle scatter on scroll-out
|
||||
- Photos/videos appear in a front-view lightbox on click
|
||||
- Energy ring orbits around focused media
|
||||
=================================================================== */
|
||||
|
||||
/* ---------- Particle burst effect (CSS + JS, no Three.js needed) ---------- */
|
||||
function createParticleBurst(el: HTMLElement, color: string = '#5bd1d8') {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
const count = 12;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.className = 'webgl-particle-burst';
|
||||
const angle = (i / count) * Math.PI * 2;
|
||||
const dist = 40 + Math.random() * 60;
|
||||
const dx = Math.cos(angle) * dist;
|
||||
const dy = Math.sin(angle) * dist;
|
||||
const size = 2 + Math.random() * 4;
|
||||
|
||||
Object.assign(particle.style, {
|
||||
position: 'fixed',
|
||||
left: `${cx}px`,
|
||||
top: `${cy}px`,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
boxShadow: `0 0 ${size * 2}px ${color}`,
|
||||
pointerEvents: 'none',
|
||||
zIndex: '9999',
|
||||
transition: 'all 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
opacity: '1',
|
||||
});
|
||||
|
||||
document.body.appendChild(particle);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
particle.style.transform = `translate(${dx}px, ${dy}px) scale(0)`;
|
||||
particle.style.opacity = '0';
|
||||
});
|
||||
|
||||
setTimeout(() => particle.remove(), 700);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Scroll-triggered card wrapper ---------- */
|
||||
export function WebGLCard({ children, color = '#5bd1d8', delay = 0 }: {
|
||||
children: React.ReactNode;
|
||||
color?: string;
|
||||
delay?: number;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !hasAnimated.current) {
|
||||
setTimeout(() => {
|
||||
setVisible(true);
|
||||
hasAnimated.current = true;
|
||||
createParticleBurst(el, color);
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.2 }
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [color, delay]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`webgl-card ${visible ? 'webgl-card--visible' : ''}`}
|
||||
style={{ '--card-accent': color } as React.CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Media lightbox (photo/video front view) ---------- */
|
||||
export function MediaLightbox() {
|
||||
const [active, setActive] = useState<{ type: 'img' | 'video'; src: string; alt?: string } | null>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for clicks on photos and videos
|
||||
const handler = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Photo click
|
||||
if (target.matches('.photo-strip-img, .webgl-media-trigger img')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const img = target as HTMLImageElement;
|
||||
setActive({ type: 'img', src: img.src, alt: img.alt });
|
||||
createParticleBurst(target, '#5bd1d8');
|
||||
}
|
||||
|
||||
// Video click
|
||||
if (target.matches('.video-strip-clip, .webgl-media-trigger video')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const video = target as HTMLVideoElement;
|
||||
setActive({ type: 'video', src: video.src });
|
||||
createParticleBurst(target, '#f1c27a');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handler, true);
|
||||
return () => document.removeEventListener('click', handler, true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setActive(null);
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [active]);
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className="media-lightbox"
|
||||
onClick={(e) => { if (e.target === overlayRef.current) setActive(null); }}
|
||||
>
|
||||
{/* Energy ring animation */}
|
||||
<div className="lightbox-ring lightbox-ring--1" />
|
||||
<div className="lightbox-ring lightbox-ring--2" />
|
||||
<div className="lightbox-ring lightbox-ring--3" />
|
||||
|
||||
<div className="lightbox-content">
|
||||
{active.type === 'img' ? (
|
||||
<img src={active.src} alt={active.alt || ''} className="lightbox-media" />
|
||||
) : (
|
||||
<video src={active.src} controls autoPlay muted loop className="lightbox-media" />
|
||||
)}
|
||||
<button className="lightbox-close" onClick={() => setActive(null)} aria-label="Fermer">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scroll-triggered section with atomic reveal ---------- */
|
||||
export function WebGLSection({ children, id, className = '' }: {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setVisible(entry.isIntersecting);
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={`webgl-section ${visible ? 'webgl-section--visible' : ''} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+36
-235
@@ -1,6 +1,5 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import AtomGlitch from '@/components/AtomGlitch';
|
||||
import '@/styles/global.css';
|
||||
import '@/styles/home-workbench.css';
|
||||
import SiteHeader from '@/components/SiteHeader.astro';
|
||||
@@ -30,114 +29,72 @@ const title = isLaunched
|
||||
? "Systèmes électroniques spécifiques | L'électron rare"
|
||||
: "L'Electron Rare — Bientôt";
|
||||
const description = isLaunched
|
||||
? "L'Électron Rare conçoit, met au point et fiabilise des systèmes électroniques spécifiques en électronique, automatisme, énergie et stockage."
|
||||
? "Conception, mise au point et fiabilisation de systèmes électroniques spécifiques."
|
||||
: "L'Electron Rare arrive. Systèmes électroniques spécifiques, formations, prototypage.";
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description}>
|
||||
{isLaunched ? (
|
||||
<!-- ============ SITE COMPLET (après 1er mai) ============ -->
|
||||
<Fragment>
|
||||
<SiteHeader brandHref="#top" items={headerItems} quickHref="#contact" />
|
||||
|
||||
<main id="main-content" class="site-shell studio-structure pb-12 pt-3">
|
||||
<!-- Hero avec WebGL en background -->
|
||||
<section class="structure-grid structure-grid--hero hero-webgl-wrapper" data-reveal>
|
||||
<div class="hero-webgl-bg">
|
||||
<AtomGlitch client:visible />
|
||||
</div>
|
||||
<div class="structure-cell structure-cell--hero hero-webgl-content">
|
||||
<section class="structure-grid structure-grid--hero" data-reveal>
|
||||
<div class="structure-cell structure-cell--hero">
|
||||
<Hero />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="structure-grid structure-grid--systems" data-reveal>
|
||||
<div class="structure-cell structure-cell--about">
|
||||
<About />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="structure-grid structure-grid--cases" data-reveal>
|
||||
<div class="structure-cell structure-cell--cases">
|
||||
<CaseStudies />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="photo-strip" aria-label="Photos de terrain" data-reveal>
|
||||
<div class="photo-strip-track">
|
||||
<img src={withSiteBase('/assets/photos/pcb-teensy-led-kxkm.webp')} alt="PCB custom Teensy LED — projet spectacle vivant KXKM" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/platine-automate-cablage.webp')} alt="Câblage automate industriel — intervention terrain" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/bench-bms-batteries-xt60.webp')} alt="Banc de test BMS batteries — prototypage" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/automate-siemens-s7.webp')} alt="Automate Siemens S7 — diagnostic industriel" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/soudure-pcb-composants.webp')} alt="Soudure PCB — formation et prototypage" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/oscilloscope-philips-vintage.webp')} alt="Instrumentation et mesure — oscilloscope" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/rack-cbtc-lon-connecteurs.webp')} alt="Rack CBTC transport — connectique industrielle" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/pcb-embarquee-boitier-test.webp')} alt="Carte embarquée en test — validation prototype" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/armoire-automate-schneider.webp')} alt="Armoire Schneider — automatisme industriel" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/outillage-atelier-pro.webp')} alt="Atelier professionnel — outillage organisé" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="video-strip" aria-label="Vidéos de terrain" data-reveal>
|
||||
<div class="video-strip-track">
|
||||
<video src={withSiteBase('/assets/videos/video-bench-electronique.mp4')} poster={withSiteBase('/assets/videos/video-bench-electronique-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Bench électronique en action"></video>
|
||||
<video src={withSiteBase('/assets/videos/video-test-prototype.mp4')} poster={withSiteBase('/assets/videos/video-test-prototype-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Test de prototype"></video>
|
||||
<video src={withSiteBase('/assets/videos/video-instrumentation.mp4')} poster={withSiteBase('/assets/videos/video-instrumentation-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Instrumentation et mesure"></video>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="structure-grid structure-grid--sprints" data-reveal>
|
||||
<div class="structure-cell structure-cell--sprints">
|
||||
<GraphicSprints />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="structure-grid structure-grid--faq" data-reveal>
|
||||
<div class="structure-cell structure-cell--faq">
|
||||
<Faq />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="structure-grid structure-grid--conversion" data-reveal>
|
||||
<div class="structure-cell structure-cell--contact">
|
||||
<Contact />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer border-t border-[var(--line)] py-6 text-sm text-[var(--text-muted)]">
|
||||
<div class="site-shell footer-grid footer-grid--enhanced">
|
||||
<div class="footer-col footer-col--brand">
|
||||
<p class="m-0 footer-title">L'électron rare</p>
|
||||
<div class="footer-brand-meta">
|
||||
<p class="mb-0 footer-copy">© 2026 Clément Saillant - systèmes électroniques spécifiques, conseil, formation — industrie, culture et projets multi-techniques.</p>
|
||||
<p class="mb-0 footer-copy footer-copy--right">Contact direct : LinkedIn DM ou contact@lelectronrare.fr</p>
|
||||
</div>
|
||||
<p class="mb-0 footer-copy">© 2026 Clément Saillant</p>
|
||||
</div>
|
||||
<nav aria-label="Ressources studio" class="footer-links footer-links--single-rail">
|
||||
<nav aria-label="Ressources" class="footer-links footer-links--single-rail">
|
||||
<a href="#a-propos" class="footer-link">Approche</a>
|
||||
<a href="#graphic-sprints-title" class="footer-link">Missions</a>
|
||||
<a href={formationHref} class="footer-link">Formation</a>
|
||||
<a href="#faq" class="footer-link">FAQ</a>
|
||||
<a href="#contact" class="footer-link">Contact</a>
|
||||
<a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener noreferrer me" {...trackAttrs(TRACK_EVENTS.outboundLinkedinContact, 'linkedin.com')} class="footer-link">LinkedIn</a>
|
||||
<a href="https://github.com/electron-rare/" target="_blank" rel="noopener noreferrer me" {...trackAttrs(TRACK_EVENTS.outboundGithubContact, 'github.com')} class="footer-link">GitHub</a>
|
||||
<a href="https://blog.saillant.cc" target="_blank" rel="noopener noreferrer me" class="footer-link">Blog technique</a>
|
||||
<a href={mentionsHref} class="footer-link">Mentions légales</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</Fragment>
|
||||
) : (
|
||||
<!-- ============ COUNTDOWN (avant 1er mai) ============ -->
|
||||
<!-- COUNTDOWN — WebGL placeholder -->
|
||||
<main class="countdown-page">
|
||||
<div class="webgl-bg">
|
||||
<AtomGlitch client:load />
|
||||
</div>
|
||||
<!-- WebGL goes here -->
|
||||
<div class="webgl-bg" id="webgl-countdown"></div>
|
||||
|
||||
<div class="overlay">
|
||||
<div class="top-bar">
|
||||
<div class="logo-sprite" id="logo-sprite" aria-label="L'Electron Rare — logo animé"></div>
|
||||
<div class="logo-sprite" id="logo-sprite" aria-label="L'Electron Rare"></div>
|
||||
<span class="brand-mark">L'ELECTRON RARE</span>
|
||||
</div>
|
||||
|
||||
@@ -153,11 +110,10 @@ const description = isLaunched
|
||||
<span class="bottom-right">mai 2026</span>
|
||||
</div>
|
||||
|
||||
<!-- Cartouche footer -->
|
||||
<div class="cartouche">
|
||||
<p class="cartouche-title">L'électron rare</p>
|
||||
<p class="cartouche-copy">© 2026 Clément Saillant — systèmes électroniques spécifiques, conseil, formation — industrie, culture et projets multi-techniques.</p>
|
||||
<p class="cartouche-contact">Contact direct : <a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener">LinkedIn DM</a> ou <a href="mailto:contact@lelectronrare.fr">contact@lelectronrare.fr</a></p>
|
||||
<p class="cartouche-contact">Contact : <a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener">LinkedIn</a> · <a href="mailto:contact@lelectronrare.fr">contact@lelectronrare.fr</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -186,8 +142,9 @@ const description = isLaunched
|
||||
let msgIndex = Math.floor(Math.random() * messages.length);
|
||||
|
||||
function rotateMessage() {
|
||||
const bubble = document.getElementById('tagline-bubble')!;
|
||||
const el = document.getElementById('tagline')!;
|
||||
const bubble = document.getElementById('tagline-bubble');
|
||||
const el = document.getElementById('tagline');
|
||||
if (!bubble || !el) return;
|
||||
bubble.style.opacity = '0';
|
||||
bubble.style.transform = 'translateY(4px)';
|
||||
setTimeout(() => {
|
||||
@@ -198,8 +155,8 @@ const description = isLaunched
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const sprite = document.getElementById('logo-sprite')!;
|
||||
sprite.style.backgroundImage = `url(/assets/brand/logo-mark.png)`;
|
||||
const sprite = document.getElementById('logo-sprite');
|
||||
if (sprite) sprite.style.backgroundImage = 'url(/assets/brand/logo-mark.png)';
|
||||
|
||||
rotateMessage();
|
||||
setInterval(rotateMessage, 8000);
|
||||
@@ -207,181 +164,25 @@ const description = isLaunched
|
||||
)}
|
||||
|
||||
<style>
|
||||
/* ============ COUNTDOWN STYLES ============ */
|
||||
.countdown-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family: 'Manrope', -apple-system, 'SF Pro Display', 'Helvetica Neue', sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
.webgl-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.overlay {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: clamp(20px, 3vw, 40px) clamp(24px, 5vw, 60px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.overlay > * { pointer-events: none; }
|
||||
.overlay a, .overlay .tagline-area { pointer-events: auto; }
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
animation: fade-in 0.8s ease both;
|
||||
}
|
||||
.logo-sprite {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
filter: brightness(1.2) drop-shadow(0 0 12px rgba(91, 209, 216, 0.4));
|
||||
}
|
||||
.brand-mark {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.25em;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.center-spacer {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: clamp(20px, 3vw, 40px);
|
||||
}
|
||||
.tagline-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 36px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 8px;
|
||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||
animation: fade-in 0.8s 1.1s ease both;
|
||||
}
|
||||
.tagline-cursor {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 14px;
|
||||
color: #5bd1d8;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
.tagline-text {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: clamp(11px, 1.3vw, 14px);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.bottom-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
animation: fade-in 0.8s 1.2s ease both;
|
||||
}
|
||||
.bottom-left, .bottom-right {
|
||||
font-size: clamp(10px, 1.1vw, 12px);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ============ CARTOUCHE FOOTER ============ */
|
||||
.cartouche {
|
||||
position: fixed;
|
||||
bottom: clamp(16px, 2vw, 24px);
|
||||
right: clamp(16px, 2vw, 24px);
|
||||
max-width: 320px;
|
||||
padding: 14px 18px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
pointer-events: auto;
|
||||
animation: fade-in 0.8s 1.5s ease both;
|
||||
}
|
||||
.cartouche-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 0 6px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.cartouche-copy {
|
||||
font-size: 9px;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
margin: 0 0 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cartouche-contact {
|
||||
font-size: 9px;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cartouche-contact a {
|
||||
color: #5bd1d8;
|
||||
text-decoration: none;
|
||||
}
|
||||
.cartouche-contact a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cartouche {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
max-width: 100%;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ HERO WEBGL (site complet) ============ */
|
||||
.hero-webgl-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero-webgl-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hero-webgl-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.bottom-bar { flex-direction: column; align-items: center; gap: 4px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.logo-sprite, .top-bar, .tagline-area, .bottom-bar { animation: none; opacity: 1; }
|
||||
.webgl-bg, .hero-webgl-bg { display: none; }
|
||||
}
|
||||
.countdown-page { position:relative; min-height:100vh; overflow:hidden; background:#000; color:#fff; font-family:'Manrope',-apple-system,sans-serif; }
|
||||
.webgl-bg { position:fixed; inset:0; z-index:0; width:100vw; height:100vh; pointer-events:auto; }
|
||||
.overlay { position:relative; z-index:10; min-height:100vh; display:flex; flex-direction:column; justify-content:space-between; padding:clamp(20px,3vw,40px) clamp(24px,5vw,60px); pointer-events:none; }
|
||||
.overlay > * { pointer-events:none; }
|
||||
.overlay a { pointer-events:auto; }
|
||||
.top-bar { display:flex; align-items:center; gap:16px; }
|
||||
.logo-sprite { width:48px; height:48px; background-size:contain; background-repeat:no-repeat; filter:brightness(1.2) drop-shadow(0 0 12px rgba(91,209,216,0.4)); }
|
||||
.brand-mark { font-size:11px; font-weight:600; letter-spacing:0.25em; color:rgba(255,255,255,0.5); text-transform:uppercase; }
|
||||
.center-spacer { flex:1; display:flex; align-items:center; justify-content:center; }
|
||||
.tagline-area { display:flex; align-items:center; gap:8px; padding:10px 20px; background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.06); border-radius:8px; transition:opacity 0.4s ease, transform 0.4s ease; pointer-events:auto; }
|
||||
.tagline-cursor { font-family:monospace; font-size:14px; color:#5bd1d8; animation:blink 1s step-end infinite; }
|
||||
@keyframes blink { 50% { opacity:0; } }
|
||||
.tagline-text { font-family:monospace; font-size:clamp(11px,1.3vw,14px); color:rgba(255,255,255,0.4); }
|
||||
.bottom-bar { display:flex; justify-content:space-between; align-items:flex-end; }
|
||||
.bottom-left, .bottom-right { font-size:clamp(10px,1.1vw,12px); color:rgba(255,255,255,0.15); letter-spacing:0.08em; text-transform:uppercase; }
|
||||
.cartouche { position:fixed; bottom:clamp(16px,2vw,24px); right:clamp(16px,2vw,24px); max-width:320px; padding:14px 18px; background:rgba(0,0,0,0.5); backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,0.06); border-radius:12px; pointer-events:auto; }
|
||||
.cartouche-title { font-size:13px; font-weight:700; color:rgba(255,255,255,0.6); margin:0 0 6px; }
|
||||
.cartouche-copy { font-size:9px; color:rgba(255,255,255,0.25); margin:0 0 4px; line-height:1.4; }
|
||||
.cartouche-contact { font-size:9px; color:rgba(255,255,255,0.25); margin:0; }
|
||||
.cartouche-contact a { color:#5bd1d8; text-decoration:none; }
|
||||
@media (max-width:640px) { .bottom-bar{flex-direction:column;align-items:center;gap:4px;} .cartouche{position:relative;bottom:auto;right:auto;max-width:100%;margin-top:12px;text-align:center;} }
|
||||
</style>
|
||||
|
||||
+75
-757
@@ -1,12 +1,8 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import WebGLBackground from '@/components/WebGLBackground';
|
||||
import { MediaLightbox } from '@/components/WebGLCards';
|
||||
import '@/styles/global.css';
|
||||
import '@/styles/home-workbench.css';
|
||||
import '@/styles/webgl-cards.css';
|
||||
import SiteHeader from '@/components/SiteHeader.astro';
|
||||
// Hero removed — PCB 3D is the hero
|
||||
import { About } from '@/components/sections/About';
|
||||
import { CaseStudies } from '@/components/sections/CaseStudies';
|
||||
import { GraphicSprints } from '@/components/sections/GraphicSprints';
|
||||
@@ -27,7 +23,7 @@ const headerItems = [
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<!-- Loading screen — shows BMU photo while WebGL loads -->
|
||||
<!-- Loading screen — BMU photo while WebGL loads -->
|
||||
<div class="loading-screen" id="loading-screen">
|
||||
<div class="loading-content">
|
||||
<img src="/assets/photos/bmu-assembly.jpg" alt="BMU v2 Assembly" class="loading-bmu-img" />
|
||||
@@ -35,124 +31,67 @@ const headerItems = [
|
||||
<div class="loading-bar">
|
||||
<div class="loading-bar-fill" id="loading-bar-fill"></div>
|
||||
</div>
|
||||
<p class="loading-status" id="loading-status">Chargement du PCB 3D...</p>
|
||||
<p class="loading-status" id="loading-status">Chargement...</p>
|
||||
<a href="/static/" class="loading-static-btn">Version sans 3D →</a>
|
||||
<p class="loading-tagline">électronique · automatisme · énergie</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebGL background layer — fixed, subtle, always visible -->
|
||||
<WebGLBackground client:load />
|
||||
<!-- Media lightbox (click photos/videos to view full) -->
|
||||
<MediaLightbox client:load />
|
||||
<!-- WebGL background placeholder — NEW WEBGL GOES HERE -->
|
||||
<div id="webgl-container"></div>
|
||||
|
||||
<SiteHeader brandHref="#top" items={headerItems} quickHref="#contact" />
|
||||
|
||||
<main id="main-content" class="dark-site">
|
||||
<!-- No HTML hero — the PCB 3D IS the hero. First spacer = overview of the board -->
|
||||
<div class="pcb-spacer pcb-spacer--hero" id="top" aria-hidden="true"></div>
|
||||
|
||||
<!-- ABOUT -->
|
||||
<section class="section-dark webgl-section" id="a-propos">
|
||||
<div class="section-content webgl-card" style="--card-accent: #5bd1d8">
|
||||
<section class="section-dark" id="a-propos">
|
||||
<div class="section-content">
|
||||
<About />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="pcb-spacer" aria-hidden="true"></div>
|
||||
|
||||
<!-- CASE STUDIES -->
|
||||
<section class="section-dark webgl-section">
|
||||
<div class="section-content webgl-card" style="--card-accent: #f1c27a">
|
||||
<section class="section-dark">
|
||||
<div class="section-content">
|
||||
<CaseStudies />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="pcb-spacer" aria-hidden="true"></div>
|
||||
|
||||
<!-- PHOTO CAROUSEL (atomic orbit) -->
|
||||
<section class="section-dark webgl-section atomic-carousel" aria-label="Photos de terrain">
|
||||
<div class="section-content">
|
||||
<h2 style="color: rgba(255,255,255,0.4); font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; text-align: center; margin-bottom: 8px;">Terrain</h2>
|
||||
<section class="photo-strip section-dark" aria-label="Photos de terrain">
|
||||
<div class="photo-strip-track">
|
||||
<img src={withSiteBase('/assets/photos/pcb-teensy-led-kxkm.webp')} alt="PCB custom Teensy LED" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/platine-automate-cablage.webp')} alt="Câblage automate industriel" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/bench-bms-batteries-xt60.webp')} alt="Banc de test BMS batteries" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/automate-siemens-s7.webp')} alt="Automate Siemens S7" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/soudure-pcb-composants.webp')} alt="Soudure PCB" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/oscilloscope-philips-vintage.webp')} alt="Oscilloscope" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
</div>
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--prev" data-carousel="photos" data-dir="-1" aria-label="Precedent">‹</button>
|
||||
<div class="atomic-carousel-orbit" id="photo-orbit">
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/pcb-teensy-led-kxkm.webp')} alt="PCB custom Teensy LED — projet spectacle vivant KXKM" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/platine-automate-cablage.webp')} alt="Câblage automate industriel" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/bench-bms-batteries-xt60.webp')} alt="Banc de test BMS batteries" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/automate-siemens-s7.webp')} alt="Automate Siemens S7" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/soudure-pcb-composants.webp')} alt="Soudure PCB composants" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/oscilloscope-philips-vintage.webp')} alt="Oscilloscope vintage" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/rack-cbtc-lon-connecteurs.webp')} alt="Rack CBTC connectique" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/pcb-embarquee-boitier-test.webp')} alt="Carte embarquée en test" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/armoire-automate-schneider.webp')} alt="Armoire Schneider" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/outillage-atelier-pro.webp')} alt="Atelier professionnel" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--next" data-carousel="photos" data-dir="1" aria-label="Suivant">›</button>
|
||||
<div class="atomic-carousel-track" id="photo-track"></div>
|
||||
</section>
|
||||
|
||||
<!-- VIDEO CAROUSEL (atomic orbit) -->
|
||||
<section class="section-dark webgl-section atomic-carousel" aria-label="Vidéos de terrain">
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--prev" data-carousel="videos" data-dir="-1" aria-label="Precedent">‹</button>
|
||||
<div class="atomic-carousel-orbit" id="video-orbit">
|
||||
<div class="atomic-carousel-item" style="width: 360px; height: 200px;">
|
||||
<video src={withSiteBase('/assets/videos/video-bench-electronique.mp4')} poster={withSiteBase('/assets/videos/video-bench-electronique-poster.webp')} muted loop playsinline preload="none" class="video-strip-clip" aria-label="Bench électronique"></video>
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 360px; height: 200px;">
|
||||
<video src={withSiteBase('/assets/videos/video-test-prototype.mp4')} poster={withSiteBase('/assets/videos/video-test-prototype-poster.webp')} muted loop playsinline preload="none" class="video-strip-clip" aria-label="Test prototype"></video>
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 360px; height: 200px;">
|
||||
<video src={withSiteBase('/assets/videos/video-instrumentation.mp4')} poster={withSiteBase('/assets/videos/video-instrumentation-poster.webp')} muted loop playsinline preload="none" class="video-strip-clip" aria-label="Instrumentation"></video>
|
||||
</div>
|
||||
</div>
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--next" data-carousel="videos" data-dir="1" aria-label="Suivant">›</button>
|
||||
<div class="atomic-carousel-track" id="video-track"></div>
|
||||
</section>
|
||||
|
||||
<div class="pcb-spacer" aria-hidden="true"></div>
|
||||
|
||||
<!-- SPRINTS -->
|
||||
<section class="section-dark webgl-section" id="graphic-sprints-title">
|
||||
<div class="section-content webgl-card" style="--card-accent: #b6d18f">
|
||||
<section class="section-dark" id="graphic-sprints-title">
|
||||
<div class="section-content">
|
||||
<GraphicSprints />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="pcb-spacer" aria-hidden="true"></div>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section class="section-dark webgl-section" id="faq">
|
||||
<div class="section-content webgl-card" style="--card-accent: #5bd1d8">
|
||||
<section class="section-dark" id="faq">
|
||||
<div class="section-content">
|
||||
<Faq />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CONTACT — hidden by default, appears as 3D tile on click or end of scroll -->
|
||||
<section class="section-dark section-contact contact-3d-popup" id="contact">
|
||||
<div class="contact-3d-backdrop" id="contact-backdrop"></div>
|
||||
<div class="section-content contact-3d-tile" id="contact-tile">
|
||||
<button class="contact-3d-close" id="contact-close" aria-label="Fermer">✕</button>
|
||||
<div class="pcb-spacer" aria-hidden="true"></div>
|
||||
|
||||
<section class="section-dark" id="contact">
|
||||
<div class="section-content">
|
||||
<Contact />
|
||||
</div>
|
||||
</section>
|
||||
@@ -163,697 +102,76 @@ const headerItems = [
|
||||
<div class="footer-col footer-col--brand">
|
||||
<p class="m-0 footer-title">L'électron rare</p>
|
||||
<div class="footer-brand-meta">
|
||||
<p class="mb-0 footer-copy">© 2026 Clément Saillant - systèmes électroniques spécifiques, conseil, formation — industrie, culture et projets multi-techniques.</p>
|
||||
<p class="mb-0 footer-copy footer-copy--right">Contact direct : LinkedIn DM ou contact@lelectronrare.fr</p>
|
||||
<p class="mb-0 footer-copy">© 2026 Clément Saillant — systèmes électroniques spécifiques, conseil, formation.</p>
|
||||
<p class="mb-0 footer-copy">Contact : <a href="mailto:contact@lelectronrare.fr">contact@lelectronrare.fr</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Ressources studio" class="footer-links footer-links--single-rail">
|
||||
<nav aria-label="Ressources" class="footer-links footer-links--single-rail">
|
||||
<a href="#a-propos" class="footer-link">Approche</a>
|
||||
<a href="#graphic-sprints-title" class="footer-link">Missions</a>
|
||||
<a href={formationHref} class="footer-link">Formation</a>
|
||||
<a href="#faq" class="footer-link">FAQ</a>
|
||||
<a href="#contact" class="footer-link">Contact</a>
|
||||
<a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener noreferrer me" {...trackAttrs(TRACK_EVENTS.outboundLinkedinContact, 'linkedin.com')} class="footer-link">LinkedIn</a>
|
||||
<a href="https://github.com/electron-rare/" target="_blank" rel="noopener noreferrer me" {...trackAttrs(TRACK_EVENTS.outboundGithubContact, 'github.com')} class="footer-link">GitHub</a>
|
||||
<a href="https://blog.saillant.cc" target="_blank" rel="noopener noreferrer me" class="footer-link">Blog technique</a>
|
||||
<a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener" class="footer-link">LinkedIn</a>
|
||||
<a href="https://github.com/electron-rare/" target="_blank" rel="noopener" class="footer-link">GitHub</a>
|
||||
<a href={mentionsHref} class="footer-link">Mentions légales</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
/* ============ DARK TECH THEME ============ */
|
||||
.dark-site {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: transparent;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.section-dark {
|
||||
position: relative;
|
||||
padding: clamp(40px, 8vw, 100px) 0;
|
||||
}
|
||||
|
||||
.section-hero {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 clamp(20px, 4vw, 48px);
|
||||
}
|
||||
|
||||
/* Glass cards for content sections */
|
||||
.section-dark :global(.circuit-board-section),
|
||||
.section-dark :global(.about-card),
|
||||
.section-dark :global(.case-card),
|
||||
.section-dark :global(.sprint-card) {
|
||||
background: rgba(255, 255, 255, 0.03) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
/* Text overrides for dark */
|
||||
.section-dark :global(h1),
|
||||
.section-dark :global(h2),
|
||||
.section-dark :global(h3),
|
||||
.section-dark :global(.section-title),
|
||||
.section-dark :global(.hero-title-word) {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.section-dark :global(p),
|
||||
.section-dark :global(li),
|
||||
.section-dark :global(.about-bio),
|
||||
.section-dark :global(.case-text),
|
||||
.section-dark :global(.sprint-deliverables) {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.section-dark :global(a) {
|
||||
color: #5bd1d8 !important;
|
||||
}
|
||||
|
||||
.section-dark :global(.hero-slogan-char) {
|
||||
color: rgba(91, 209, 216, 0.8) !important;
|
||||
}
|
||||
|
||||
/* Photo strip dark */
|
||||
.section-dark.photo-strip {
|
||||
padding: clamp(20px, 4vw, 40px) 0;
|
||||
}
|
||||
.section-dark :global(.photo-strip-img) {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.section-dark :global(.photo-strip-img:hover) {
|
||||
opacity: 1;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* Video strip dark */
|
||||
.section-dark :global(.video-strip-clip) {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* FAQ dark */
|
||||
.section-dark :global(.faq-question) {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.section-dark :global(.faq-answer) {
|
||||
color: rgba(255, 255, 255, 0.6) !important;
|
||||
}
|
||||
.section-dark :global(details) {
|
||||
border-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Chips/badges glow */
|
||||
.section-dark :global(.about-chip) {
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
background: rgba(255, 255, 255, 0.04) !important;
|
||||
}
|
||||
|
||||
/* Sprint progress bars glow */
|
||||
.section-dark :global(.sprint-progress-fill) {
|
||||
box-shadow: 0 0 12px rgba(91, 209, 216, 0.4);
|
||||
}
|
||||
|
||||
/* CTA buttons */
|
||||
.section-dark :global(.btn-primary),
|
||||
.section-dark :global([data-track]) {
|
||||
background: rgba(91, 209, 216, 0.15) !important;
|
||||
border: 1px solid rgba(91, 209, 216, 0.3) !important;
|
||||
color: #5bd1d8 !important;
|
||||
}
|
||||
.section-dark :global(.btn-primary:hover) {
|
||||
background: rgba(91, 209, 216, 0.25) !important;
|
||||
}
|
||||
|
||||
/* Contact form dark (Minitel) */
|
||||
.section-dark :global(.minitel-body) {
|
||||
background: rgba(0, 0, 0, 0.6) !important;
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.section-dark :global(.minitel-input),
|
||||
.section-dark :global(.minitel-textarea),
|
||||
.section-dark :global(.minitel-select) {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
/* Hero background image — very subtle dark blend */
|
||||
.section-dark :global(.figma-lab-hero-grid) {
|
||||
position: relative;
|
||||
}
|
||||
.section-dark :global(.hero-panel-image) {
|
||||
opacity: 0.1 !important;
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
/* ============ DARK FOOTER ============ */
|
||||
.dark-footer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 24px 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.dark-footer :global(.footer-title) {
|
||||
color: #ffffff;
|
||||
}
|
||||
.dark-footer :global(.footer-copy) {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.dark-footer :global(.footer-link) {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
.dark-footer :global(.footer-link:hover) {
|
||||
color: #5bd1d8 !important;
|
||||
}
|
||||
|
||||
/* ============ LOADING SCREEN ============ */
|
||||
.loading-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 1s ease, visibility 1s ease;
|
||||
}
|
||||
.loading-screen.loading-done {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
animation: loading-fade-in 0.8s ease both;
|
||||
}
|
||||
@keyframes loading-fade-in {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.loading-bmu-img {
|
||||
width: clamp(200px, 50vw, 400px);
|
||||
height: auto;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(91, 209, 216, 0.15);
|
||||
box-shadow: 0 0 40px rgba(91, 209, 216, 0.08), 0 12px 40px rgba(0,0,0,0.5);
|
||||
animation: loading-float 4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes loading-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.loading-title {
|
||||
font-family: 'Manrope', -apple-system, sans-serif;
|
||||
font-size: clamp(28px, 5vw, 48px);
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
letter-spacing: -0.03em;
|
||||
text-shadow: 0 0 30px rgba(91, 209, 216, 0.2);
|
||||
}
|
||||
.loading-electric {
|
||||
color: #5bd1d8;
|
||||
text-shadow: 0 0 15px rgba(91, 209, 216, 0.6);
|
||||
}
|
||||
.loading-bar {
|
||||
width: clamp(200px, 40vw, 300px);
|
||||
height: 2px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.loading-bar-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, #5bd1d8, #f1c27a);
|
||||
border-radius: 1px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.loading-status {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
margin: 0;
|
||||
}
|
||||
.loading-static-btn {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: rgba(91, 209, 216, 0.6);
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(91, 209, 216, 0.15);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.loading-static-btn:hover {
|
||||
color: #5bd1d8;
|
||||
border-color: rgba(91, 209, 216, 0.4);
|
||||
background: rgba(91, 209, 216, 0.05);
|
||||
}
|
||||
.loading-tagline {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.12);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ============ HERO BMU ============ */
|
||||
.hero-bmu {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: clamp(60px, 10vw, 120px) clamp(20px, 4vw, 48px);
|
||||
gap: clamp(24px, 4vw, 48px);
|
||||
}
|
||||
|
||||
.hero-bmu-title {
|
||||
animation: hero-title-in 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
@keyframes hero-title-in {
|
||||
from { opacity: 0; transform: translateY(30px); filter: blur(8px); }
|
||||
to { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
}
|
||||
|
||||
.hero-bmu-h1 {
|
||||
font-family: 'Manrope', -apple-system, sans-serif;
|
||||
font-size: clamp(36px, 7vw, 72px);
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 40px rgba(91, 209, 216, 0.2);
|
||||
}
|
||||
.hero-electric {
|
||||
color: #5bd1d8;
|
||||
text-shadow: 0 0 20px rgba(91, 209, 216, 0.6);
|
||||
}
|
||||
|
||||
.hero-bmu-sub {
|
||||
font-size: clamp(14px, 2vw, 18px);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin: 12px 0 0;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* Navigation zones — PCB component style */
|
||||
.hero-bmu-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: clamp(10px, 2vw, 20px);
|
||||
max-width: 700px;
|
||||
animation: hero-nav-in 1s 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
@keyframes hero-nav-in {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.hero-bmu-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: clamp(14px, 2vw, 24px) clamp(18px, 3vw, 32px);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 16px;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
min-width: 120px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-bmu-zone::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: 16px;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, var(--zone-color, #5bd1d8) 0%, transparent 50%);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.hero-bmu-zone:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 0 24px rgba(91, 209, 216, 0.1), 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(91, 209, 216, 0.15);
|
||||
}
|
||||
.hero-bmu-zone:hover::before { opacity: 0.5; }
|
||||
|
||||
.hero-bmu-zone--mcu { --zone-color: #5bd1d8; }
|
||||
.hero-bmu-zone--analog { --zone-color: #f1c27a; }
|
||||
.hero-bmu-zone--mission { --zone-color: #b6d18f; }
|
||||
.hero-bmu-zone--formation { --zone-color: #ff9f0a; }
|
||||
.hero-bmu-zone--contact { --zone-color: #ff6b35; }
|
||||
|
||||
.hero-bmu-zone-icon {
|
||||
font-size: 24px;
|
||||
opacity: 0.6;
|
||||
color: var(--zone-color, #5bd1d8);
|
||||
filter: drop-shadow(0 0 6px var(--zone-color, #5bd1d8));
|
||||
}
|
||||
|
||||
.hero-bmu-zone-label {
|
||||
font-family: 'Orbitron', 'Manrope', sans-serif;
|
||||
font-size: clamp(11px, 1.3vw, 14px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--zone-color, #5bd1d8);
|
||||
}
|
||||
|
||||
.hero-bmu-zone-desc {
|
||||
font-size: clamp(10px, 1vw, 12px);
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.hero-bmu-tagline {
|
||||
animation: hero-tag-in 0.8s 0.6s ease both;
|
||||
}
|
||||
@keyframes hero-tag-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.hero-bmu-tagline span {
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-bmu-nav { flex-direction: column; align-items: stretch; }
|
||||
.hero-bmu-zone { flex-direction: row; gap: 12px; text-align: left; }
|
||||
}
|
||||
|
||||
/* ============ PCB SPACER ============ */
|
||||
.pcb-spacer {
|
||||
height: 60vh;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pcb-spacer--hero {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ============ CONTACT 3D POPUP ============ */
|
||||
.contact-3d-popup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.4s ease, visibility 0.4s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contact-3d-popup.contact-3d-popup--open {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.contact-3d-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.contact-3d-tile {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 600px;
|
||||
width: 90vw;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(10, 10, 10, 0.9);
|
||||
border: 1px solid rgba(91, 209, 216, 0.15);
|
||||
border-radius: 20px;
|
||||
padding: clamp(20px, 4vw, 40px);
|
||||
box-shadow:
|
||||
0 0 40px rgba(91, 209, 216, 0.08),
|
||||
0 24px 80px rgba(0, 0, 0, 0.6);
|
||||
transform: perspective(1200px) rotateY(-15deg) rotateX(5deg) scale(0.85) translateY(40px);
|
||||
opacity: 0;
|
||||
transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.contact-3d-popup--open .contact-3d-tile {
|
||||
transform: perspective(1200px) rotateY(0deg) rotateX(0deg) scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.contact-3d-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.contact-3d-close:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #ffffff;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.section-hero { min-height: auto; }
|
||||
.contact-3d-tile { transform: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Initialize scroll-triggered webgl-card and webgl-section animations
|
||||
function initScrollAnimations() {
|
||||
const cards = document.querySelectorAll('.webgl-card');
|
||||
const sections = document.querySelectorAll('.webgl-section');
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add(
|
||||
entry.target.classList.contains('webgl-card')
|
||||
? 'webgl-card--visible'
|
||||
: 'webgl-section--visible'
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.15 }
|
||||
);
|
||||
|
||||
cards.forEach((el) => observer.observe(el));
|
||||
sections.forEach((el) => observer.observe(el));
|
||||
}
|
||||
|
||||
// 3D tilt on scroll — cards tilt based on their position in viewport
|
||||
function initScrollTilt() {
|
||||
const cards = document.querySelectorAll('.webgl-card--visible');
|
||||
function updateTilt() {
|
||||
cards.forEach((card) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const vh = window.innerHeight;
|
||||
const center = rect.top + rect.height / 2;
|
||||
const ratio = (center - vh / 2) / (vh / 2); // -1 (top) to 1 (bottom)
|
||||
const rotateX = ratio * -3;
|
||||
const rotateY = ratio * 1.5;
|
||||
(card as HTMLElement).style.transform =
|
||||
`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
|
||||
});
|
||||
requestAnimationFrame(updateTilt);
|
||||
}
|
||||
updateTilt();
|
||||
}
|
||||
|
||||
// Carousel navigation
|
||||
function initCarousels() {
|
||||
document.querySelectorAll('.atomic-carousel-nav').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const dir = parseInt(btn.getAttribute('data-dir') || '1');
|
||||
const carouselType = btn.getAttribute('data-carousel');
|
||||
const orbit = document.getElementById(carouselType === 'photos' ? 'photo-orbit' : 'video-orbit');
|
||||
if (!orbit) return;
|
||||
const itemWidth = orbit.querySelector('.atomic-carousel-item')?.clientWidth || 320;
|
||||
orbit.scrollBy({ left: dir * (itemWidth + 24), behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
// Track position indicator
|
||||
['photo-orbit', 'video-orbit'].forEach((id) => {
|
||||
const orbit = document.getElementById(id);
|
||||
const track = document.getElementById(id.replace('orbit', 'track'));
|
||||
if (!orbit || !track) return;
|
||||
orbit.addEventListener('scroll', () => {
|
||||
const pct = orbit.scrollLeft / (orbit.scrollWidth - orbit.clientWidth) * 100;
|
||||
track.style.setProperty('--track-position', `${pct}%`);
|
||||
});
|
||||
});
|
||||
|
||||
// Video hover play
|
||||
document.querySelectorAll('.atomic-carousel-item video').forEach((video) => {
|
||||
const v = video as HTMLVideoElement;
|
||||
v.closest('.atomic-carousel-item')?.addEventListener('mouseenter', () => v.play());
|
||||
v.closest('.atomic-carousel-item')?.addEventListener('mouseleave', () => { v.pause(); v.currentTime = 0; });
|
||||
});
|
||||
}
|
||||
|
||||
// Run on load and on Astro page transitions
|
||||
initScrollAnimations();
|
||||
setTimeout(initScrollTilt, 2000);
|
||||
initCarousels();
|
||||
// Contact 3D popup
|
||||
function initContactPopup() {
|
||||
const popup = document.getElementById('contact');
|
||||
const backdrop = document.getElementById('contact-backdrop');
|
||||
const closeBtn = document.getElementById('contact-close');
|
||||
if (!popup) return;
|
||||
|
||||
function openContact() {
|
||||
popup.classList.add('contact-3d-popup--open');
|
||||
}
|
||||
function closeContact() {
|
||||
popup.classList.remove('contact-3d-popup--open');
|
||||
}
|
||||
|
||||
// Open on any #contact link click
|
||||
document.querySelectorAll('a[href="#contact"]').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
openContact();
|
||||
});
|
||||
});
|
||||
|
||||
// Open at end of scroll (>95%)
|
||||
let endTriggered = false;
|
||||
window.addEventListener('scroll', () => {
|
||||
const el = document.documentElement;
|
||||
const pct = el.scrollTop / (el.scrollHeight - el.clientHeight);
|
||||
if (pct > 0.95 && !endTriggered) {
|
||||
endTriggered = true;
|
||||
openContact();
|
||||
}
|
||||
if (pct < 0.9) endTriggered = false;
|
||||
}, { passive: true });
|
||||
|
||||
// Close
|
||||
backdrop?.addEventListener('click', closeContact);
|
||||
closeBtn?.addEventListener('click', closeContact);
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeContact();
|
||||
});
|
||||
}
|
||||
|
||||
// Loading screen — fade out when WebGL canvas renders
|
||||
const loadingMessages = [
|
||||
"Chargement du PCB 3D...",
|
||||
"Initialisation des composants...",
|
||||
"Soudure des connexions...",
|
||||
"Calibration de la caméra...",
|
||||
"Vérification des pistes cuivre...",
|
||||
"Flash du rendu WebGL...",
|
||||
];
|
||||
let loadMsgIdx = 0;
|
||||
// Loading screen — auto-hide after 10s or when ready
|
||||
const loadScreen = document.getElementById('loading-screen');
|
||||
const loadBar = document.getElementById('loading-bar-fill');
|
||||
const loadStatus = document.getElementById('loading-status');
|
||||
const loadScreen = document.getElementById('loading-screen');
|
||||
|
||||
const loadInterval = setInterval(() => {
|
||||
loadMsgIdx = (loadMsgIdx + 1) % loadingMessages.length;
|
||||
if (loadStatus) loadStatus.textContent = loadingMessages[loadMsgIdx];
|
||||
if (loadBar) {
|
||||
const pct = Math.min(90, parseFloat(loadBar.style.width || '0') + 15);
|
||||
loadBar.style.width = pct + '%';
|
||||
}
|
||||
}, 800);
|
||||
const msgs = ["Chargement...", "Initialisation...", "Préparation du rendu..."];
|
||||
let idx = 0;
|
||||
const interval = setInterval(() => {
|
||||
idx = (idx + 1) % msgs.length;
|
||||
if (loadStatus) loadStatus.textContent = msgs[idx];
|
||||
if (loadBar) loadBar.style.width = Math.min(90, parseFloat(loadBar.style.width || '0') + 20) + '%';
|
||||
}, 1500);
|
||||
|
||||
// Force 10s minimum loading, then fade out
|
||||
setTimeout(() => {
|
||||
if (loadBar) loadBar.style.width = '100%';
|
||||
if (loadStatus) loadStatus.textContent = 'PCB prêt.';
|
||||
clearInterval(loadInterval);
|
||||
setTimeout(() => {
|
||||
loadScreen?.classList.add('loading-done');
|
||||
}, 600);
|
||||
if (loadStatus) loadStatus.textContent = 'Prêt.';
|
||||
clearInterval(interval);
|
||||
setTimeout(() => loadScreen?.classList.add('loading-done'), 500);
|
||||
}, 10000);
|
||||
|
||||
// Hero BMU zone smooth scroll
|
||||
document.querySelectorAll('.hero-bmu-zone[href^="#"]').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href === '#contact') return;
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(href);
|
||||
if (target) target.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
initContactPopup();
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
initScrollAnimations();
|
||||
setTimeout(initScrollTilt, 2000);
|
||||
initCarousels();
|
||||
initContactPopup();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Loading screen */
|
||||
.loading-screen { position:fixed; inset:0; z-index:9999; background:#000; display:flex; align-items:center; justify-content:center; transition:opacity 1s ease, visibility 1s ease; }
|
||||
.loading-screen.loading-done { opacity:0; visibility:hidden; pointer-events:none; }
|
||||
.loading-content { display:flex; flex-direction:column; align-items:center; text-align:center; gap:20px; padding:24px; }
|
||||
.loading-bmu-img { width:clamp(200px,50vw,400px); height:auto; border-radius:16px; border:1px solid rgba(91,209,216,0.15); box-shadow:0 0 40px rgba(91,209,216,0.08); }
|
||||
.loading-title { font-size:clamp(28px,5vw,48px); font-weight:800; color:#fff; margin:0; }
|
||||
.loading-electric { color:#5bd1d8; }
|
||||
.loading-bar { width:clamp(200px,40vw,300px); height:2px; background:rgba(255,255,255,0.06); border-radius:1px; overflow:hidden; }
|
||||
.loading-bar-fill { height:100%; width:0%; background:linear-gradient(90deg,#5bd1d8,#f1c27a); transition:width 0.3s ease; }
|
||||
.loading-status { font-family:monospace; font-size:12px; color:rgba(255,255,255,0.3); margin:0; }
|
||||
.loading-static-btn { font-size:11px; color:rgba(91,209,216,0.6); text-decoration:none; padding:8px 16px; border:1px solid rgba(91,209,216,0.15); border-radius:8px; }
|
||||
.loading-static-btn:hover { color:#5bd1d8; border-color:rgba(91,209,216,0.4); }
|
||||
.loading-tagline { font-size:11px; color:rgba(255,255,255,0.12); letter-spacing:0.1em; text-transform:uppercase; margin:0; }
|
||||
|
||||
/* Dark site base */
|
||||
.dark-site { position:relative; z-index:1; background:transparent; color:#e0e0e0; }
|
||||
.section-dark { padding:clamp(40px,8vw,100px) 0; }
|
||||
.section-content { max-width:1200px; margin:0 auto; padding:0 clamp(20px,4vw,48px); }
|
||||
.pcb-spacer { height:60vh; }
|
||||
.pcb-spacer--hero { height:100vh; }
|
||||
|
||||
/* Dark overrides */
|
||||
.section-dark :global(h1), .section-dark :global(h2), .section-dark :global(h3) { color:#fff !important; }
|
||||
.section-dark :global(p), .section-dark :global(li) { color:rgba(255,255,255,0.7) !important; }
|
||||
.section-dark :global(a) { color:#5bd1d8 !important; }
|
||||
|
||||
/* Dark footer */
|
||||
.dark-footer { position:relative; z-index:2; border-top:1px solid rgba(255,255,255,0.06); padding:24px 0; background:rgba(0,0,0,0.4); }
|
||||
.dark-footer :global(.footer-title) { color:#fff; }
|
||||
.dark-footer :global(.footer-copy) { color:rgba(255,255,255,0.4); }
|
||||
.dark-footer :global(.footer-link) { color:rgba(255,255,255,0.5) !important; }
|
||||
</style>
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
WEBGL CARDS — Atomic scroll animations + lightbox
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Card materialize animation ─────────────────────── */
|
||||
.webgl-card {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
filter: blur(6px);
|
||||
transition:
|
||||
opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.8s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
filter 0.8s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
box-shadow 0.4s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.webgl-card--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
/* Glow border on appear */
|
||||
.webgl-card--visible::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--card-accent, #5bd1d8) 0%,
|
||||
transparent 40%,
|
||||
transparent 60%,
|
||||
var(--card-accent, #5bd1d8) 100%
|
||||
);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0;
|
||||
animation: glow-border-in 1.2s 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes glow-border-in {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.6; }
|
||||
100% { opacity: 0.15; }
|
||||
}
|
||||
|
||||
/* Card hover tilt effect */
|
||||
.webgl-card--visible:hover {
|
||||
box-shadow:
|
||||
0 0 20px rgba(91, 209, 216, 0.08),
|
||||
0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-2px) scale(1.01);
|
||||
}
|
||||
|
||||
/* ── Section atomic reveal — alternate orbit direction ── */
|
||||
.webgl-section {
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* Odd sections slide from left orbit */
|
||||
.webgl-section:nth-child(odd) {
|
||||
transform: translateX(-60px) rotate(-1deg);
|
||||
}
|
||||
|
||||
/* Even sections slide from right orbit */
|
||||
.webgl-section:nth-child(even) {
|
||||
transform: translateX(60px) rotate(1deg);
|
||||
}
|
||||
|
||||
.webgl-section--visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
|
||||
/* Staggered children reveal */
|
||||
.webgl-section--visible > *:nth-child(1) { transition-delay: 0s; }
|
||||
.webgl-section--visible > *:nth-child(2) { transition-delay: 0.1s; }
|
||||
.webgl-section--visible > *:nth-child(3) { transition-delay: 0.2s; }
|
||||
.webgl-section--visible > *:nth-child(4) { transition-delay: 0.3s; }
|
||||
.webgl-section--visible > *:nth-child(5) { transition-delay: 0.4s; }
|
||||
|
||||
/* ── Photo/Video hover effect ───────────────────────── */
|
||||
.photo-strip-img,
|
||||
.video-strip-clip {
|
||||
cursor: pointer;
|
||||
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.4s ease, filter 0.3s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.photo-strip-img:hover,
|
||||
.video-strip-clip:hover {
|
||||
transform: scale(1.05) translateY(-4px);
|
||||
box-shadow:
|
||||
0 0 24px rgba(91, 209, 216, 0.15),
|
||||
0 12px 40px rgba(0, 0, 0, 0.4);
|
||||
filter: brightness(1.1);
|
||||
border-color: rgba(91, 209, 216, 0.2);
|
||||
}
|
||||
|
||||
/* ── Lightbox ───────────────────────────────────────── */
|
||||
.media-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
animation: lightbox-in 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@keyframes lightbox-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
cursor: default;
|
||||
animation: lightbox-content-in 0.5s 0.1s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@keyframes lightbox-content-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(20px);
|
||||
filter: blur(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-media {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 0 60px rgba(91, 209, 216, 0.1),
|
||||
0 24px 80px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #ffffff;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* ── Energy rings around lightbox media ──────────────── */
|
||||
.lightbox-ring {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border: 1px solid rgba(91, 209, 216, 0.15);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lightbox-ring--1 {
|
||||
width: 110%;
|
||||
height: 110%;
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
animation: ring-orbit 8s linear infinite;
|
||||
border-style: dashed;
|
||||
border-color: rgba(91, 209, 216, 0.1);
|
||||
}
|
||||
|
||||
.lightbox-ring--2 {
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
animation: ring-orbit 12s linear infinite reverse;
|
||||
border-color: rgba(241, 194, 122, 0.08);
|
||||
}
|
||||
|
||||
.lightbox-ring--3 {
|
||||
width: 130%;
|
||||
height: 60%;
|
||||
transform: translate(-50%, -50%) rotate(30deg);
|
||||
animation: ring-orbit 6s linear infinite;
|
||||
border-color: rgba(182, 209, 143, 0.06);
|
||||
}
|
||||
|
||||
@keyframes ring-orbit {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ── Particle burst (injected by JS) ─────────────────── */
|
||||
.webgl-particle-burst {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* ── 3D Tilt on scroll ──────────────────────────────── */
|
||||
.webgl-card {
|
||||
perspective: 1000px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
/* Alternating left/right orbit entry */
|
||||
.webgl-card--visible {
|
||||
animation: card-orbit-right 1.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* Even cards enter from right, odd from left */
|
||||
.webgl-section:nth-child(odd) .webgl-card--visible {
|
||||
animation-name: card-orbit-left;
|
||||
}
|
||||
.webgl-section:nth-child(even) .webgl-card--visible {
|
||||
animation-name: card-orbit-right;
|
||||
}
|
||||
|
||||
@keyframes card-orbit-left {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: perspective(1200px) translateX(-120px) rotateY(25deg) rotateZ(-3deg) scale(0.85);
|
||||
filter: blur(8px);
|
||||
}
|
||||
40% {
|
||||
opacity: 0.7;
|
||||
transform: perspective(1200px) translateX(-20px) rotateY(8deg) rotateZ(-1deg) scale(0.97);
|
||||
filter: blur(2px);
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: perspective(1200px) translateX(8px) rotateY(-2deg) rotateZ(0.5deg) scale(1.01);
|
||||
filter: blur(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: perspective(1200px) translateX(0) rotateY(0deg) rotateZ(0deg) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes card-orbit-right {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: perspective(1200px) translateX(120px) rotateY(-25deg) rotateZ(3deg) scale(0.85);
|
||||
filter: blur(8px);
|
||||
}
|
||||
40% {
|
||||
opacity: 0.7;
|
||||
transform: perspective(1200px) translateX(20px) rotateY(-8deg) rotateZ(1deg) scale(0.97);
|
||||
filter: blur(2px);
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: perspective(1200px) translateX(-8px) rotateY(2deg) rotateZ(-0.5deg) scale(1.01);
|
||||
filter: blur(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: perspective(1200px) translateX(0) rotateY(0deg) rotateZ(0deg) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mouse-driven 3D tilt on hover */
|
||||
.webgl-card--visible:hover {
|
||||
transition: box-shadow 0.3s ease;
|
||||
box-shadow:
|
||||
0 0 24px rgba(91, 209, 216, 0.08),
|
||||
0 12px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* ── Atomic carousel for photos/videos ─────────────── */
|
||||
.atomic-carousel {
|
||||
position: relative;
|
||||
padding: clamp(40px, 6vw, 80px) 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.atomic-carousel-orbit {
|
||||
display: flex;
|
||||
gap: clamp(12px, 2vw, 24px);
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
padding: 20px clamp(20px, 4vw, 48px);
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.atomic-carousel-orbit::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.atomic-carousel-item {
|
||||
flex: 0 0 auto;
|
||||
scroll-snap-align: center;
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform: perspective(800px) rotateY(0deg);
|
||||
}
|
||||
|
||||
.atomic-carousel-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(91, 209, 216, 0.08);
|
||||
pointer-events: none;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.atomic-carousel-item:hover {
|
||||
transform: perspective(800px) rotateY(-3deg) translateY(-8px) scale(1.04);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.atomic-carousel-item:hover::before {
|
||||
border-color: rgba(91, 209, 216, 0.3);
|
||||
box-shadow:
|
||||
0 0 30px rgba(91, 209, 216, 0.12),
|
||||
inset 0 0 20px rgba(91, 209, 216, 0.03);
|
||||
}
|
||||
|
||||
/* Electron dots orbiting around hovered item */
|
||||
.atomic-carousel-item:hover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #5bd1d8;
|
||||
box-shadow: 0 0 12px #5bd1d8;
|
||||
top: -3px;
|
||||
left: 50%;
|
||||
z-index: 3;
|
||||
animation: electron-orbit-item 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes electron-orbit-item {
|
||||
0% { top: -3px; left: 50%; }
|
||||
25% { top: 50%; left: calc(100% + 3px); }
|
||||
50% { top: calc(100% + 3px); left: 50%; }
|
||||
75% { top: 50%; left: -3px; }
|
||||
100% { top: -3px; left: 50%; }
|
||||
}
|
||||
|
||||
.atomic-carousel-item img,
|
||||
.atomic-carousel-item video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: filter 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.atomic-carousel-item:hover img,
|
||||
.atomic-carousel-item:hover video {
|
||||
filter: brightness(1.15) contrast(1.05);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* Carousel navigation arrows */
|
||||
.atomic-carousel-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(91, 209, 216, 0.1);
|
||||
border: 1px solid rgba(91, 209, 216, 0.2);
|
||||
color: #5bd1d8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 20;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.atomic-carousel-nav:hover {
|
||||
background: rgba(91, 209, 216, 0.2);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
box-shadow: 0 0 20px rgba(91, 209, 216, 0.2);
|
||||
}
|
||||
|
||||
.atomic-carousel-nav--prev { left: 8px; }
|
||||
.atomic-carousel-nav--next { right: 8px; }
|
||||
|
||||
/* Orbital track line under carousel */
|
||||
.atomic-carousel-track {
|
||||
height: 1px;
|
||||
margin: 16px auto 0;
|
||||
max-width: 200px;
|
||||
background: linear-gradient(90deg, transparent, rgba(91, 209, 216, 0.3), transparent);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.atomic-carousel-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #5bd1d8;
|
||||
box-shadow: 0 0 10px #5bd1d8;
|
||||
top: -3px;
|
||||
left: var(--track-position, 50%);
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
/* ── Reduced motion ──────────────────────────────────── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.webgl-card,
|
||||
.webgl-section {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
filter: none;
|
||||
transition: none;
|
||||
}
|
||||
.webgl-card--visible::before { display: none; }
|
||||
.lightbox-ring { display: none; }
|
||||
.lightbox-content { animation: none; opacity: 1; transform: none; }
|
||||
}
|
||||
Reference in New Issue
Block a user