diff --git a/components/home/HeroIllustration.tsx b/components/home/HeroIllustration.tsx index fbd4283f..f529edd5 100644 --- a/components/home/HeroIllustration.tsx +++ b/components/home/HeroIllustration.tsx @@ -18,31 +18,38 @@ function sr(seed: number) { } // ── TERRAIN HEIGHT FUNCTION ── -// This is the SINGLE SOURCE OF TRUTH for terrain elevation. -// Used by BOTH the terrain mesh AND all object placement. -const TERRAIN_R = 260; +// SINGLE SOURCE OF TRUTH for terrain elevation. +const TERRAIN_R = 400; +const TERRAIN_CURVATURE = 0.00012; // planetary curvature + function terrainY(x: number, z: number): number { const dist = Math.sqrt(x * x + z * z); const edge = Math.min(1, dist / TERRAIN_R); - const h = Math.max(0, 1 - edge * 1.3); + const h = Math.max(0, 1 - edge * 1.2); + + // Planetary curvature — drops off like the surface of a sphere + const curvature = -dist * dist * TERRAIN_CURVATURE; + + // Multi-octave noise for rolling hills let y = 0; - y += Math.sin(x * 0.05) * Math.cos(z * 0.04) * 1.3 * h; - y += Math.sin(x * 0.1 + 2) * Math.cos(z * 0.08 + 1) * 0.5 * h; - y += Math.sin(x * 0.02 + 5) * Math.cos(z * 0.025 + 3) * 1.0 * h; - y -= edge * edge * 25; - return y; + y += Math.sin(x * 0.025 + 1.2) * Math.cos(z * 0.02 + 0.8) * 3.5 * h; + y += Math.sin(x * 0.05 + 3.0) * Math.cos(z * 0.04 + 2.0) * 1.8 * h; + y += Math.sin(x * 0.1 + 5.0) * Math.cos(z * 0.08 + 4.0) * 0.6 * h; + y += Math.sin(x * 0.015) * Math.cos(z * 0.018 + 6.0) * 4.0 * h; + + // Edge drop-off + y -= edge * edge * 40; + + return y + curvature; } -// Place on terrain — EVERY object uses this function onTerrain(x: number, z: number): THREE.Vector3 { return new THREE.Vector3(x, terrainY(x, z), z); } // ── CATENARY CABLE ── -// Real catenary droop: cosh-based sag between two attachment points function catenarySag(t: number): number { - // t: 0→1 along cable. Returns droop factor: 0 at ends, -1 at center - const u = t * 2 - 1; // -1 → 1 + const u = t * 2 - 1; const cEdge = Math.cosh(2.5); return -(cEdge - Math.cosh(u * 2.5)) / (cEdge - 1); } @@ -63,17 +70,17 @@ function catenaryPoints( return pts; } -// ── POISSON DISC SAMPLING (approximate) for natural spacing ── +// ── POISSON DISC SAMPLING ── function poissonDisc( count: number, radius: number, bounds: number, seed: number, - exclusions: THREE.Vector3[] = [], exRadius = 12, + exclusions: THREE.Vector3[] = [], exRadius = 18, ): THREE.Vector3[] { const pts: THREE.Vector3[] = []; - const attempts = count * 12; + const attempts = count * 15; for (let i = 0; i < attempts && pts.length < count; i++) { const x = (sr(seed + i * 17) - 0.5) * bounds * 2; const z = (sr(seed + i * 23 + 1000) - 0.5) * bounds * 2; - if (Math.sqrt(x * x + z * z) > TERRAIN_R * 0.75) continue; + if (Math.sqrt(x * x + z * z) > TERRAIN_R * 0.7) continue; if (pts.some(p => Math.hypot(x - p.x, z - p.z) < radius)) continue; if (exclusions.some(p => Math.hypot(x - p.x, z - p.z) < exRadius)) continue; pts.push(onTerrain(x, z)); @@ -83,29 +90,34 @@ function poissonDisc( function nearest(pt: THREE.Vector3, list: THREE.Vector3[]): THREE.Vector3 { let best = list[0], bestD = Infinity; - for (const c of list) { const d = Math.hypot(pt.x - c.x, pt.z - c.z); if (d < bestD) { bestD = d; best = c; } } + for (const c of list) { + const d = Math.hypot(pt.x - c.x, pt.z - c.z); + if (d < bestD) { bestD = d; best = c; } + } return best; } // ═══════════════════════════════════════════════════════════════ -// PROCEDURAL WORLD — all computed from algorithms +// PROCEDURAL WORLD // ═══════════════════════════════════════════════════════════════ -// Step 1: Place hub substations at fixed strategic positions -const HUBS = [onTerrain(-6, 14), onTerrain(50, -2)]; +// Hub substations — central distribution points +const HUBS = [onTerrain(-10, 20), onTerrain(60, -5)]; -// Step 2: Place cities at the periphery +// Cities at the periphery with their substations const CITY_DATA = [ - { city: onTerrain(100, -55), sub: onTerrain(84, -46) }, - { city: onTerrain(96, 58), sub: onTerrain(82, 50) }, - { city: onTerrain(-4, -102), sub: onTerrain(-3, -86) }, + { city: onTerrain(150, -80), sub: onTerrain(125, -65) }, + { city: onTerrain(140, 85), sub: onTerrain(115, 70) }, + { city: onTerrain(-15, -150), sub: onTerrain(-12, -125) }, + { city: onTerrain(-140, 40), sub: onTerrain(-115, 35) }, + { city: onTerrain(30, 160), sub: onTerrain(28, 135) }, ]; -// Step 3: Wind farm zones — use noise to find suitable areas +// Wind farm generation type WindFarm = { sub: THREE.Vector3; turbines: THREE.Vector3[]; - chains: THREE.Vector3[][]; // turbine→turbine→sub rows + chains: THREE.Vector3[][]; }; function placeWindFarm( @@ -123,8 +135,8 @@ function placeWindFarm( for (let r = 0; r < rows; r++) { const row: THREE.Vector3[] = []; for (let c = 0; c < cols; c++) { - const lx = (c - (cols - 1) / 2) * spacing + (sr(seed + r * 10 + c) - 0.5) * 2; - const lz = (r - (rows - 1) / 2) * spacing + (sr(seed + r * 10 + c + 50) - 0.5) * 2; + const lx = (c - (cols - 1) / 2) * spacing + (sr(seed + r * 10 + c) - 0.5) * 3; + const lz = (r - (rows - 1) / 2) * spacing + (sr(seed + r * 10 + c + 50) - 0.5) * 3; const wx = cx + lx * co - lz * si; const wz = cz + lx * si + lz * co; const pos = onTerrain(wx, wz); @@ -135,34 +147,36 @@ function placeWindFarm( } const sub = onTerrain(cx + subOffX, cz + subOffZ); - - // Chain: each row connects turbine→turbine→sub const chains: THREE.Vector3[][] = grid.map(row => [...row, sub]); return { sub, turbines, chains }; } const WIND_FARMS: WindFarm[] = [ - placeWindFarm(-82, 62, 3, 2, 14, 32, -10, 5, 101), - placeWindFarm(16, 90, 3, 2, 13, -22, -16, 12, 201), - placeWindFarm(-50, -22, 2, 3, 12, 20, 12, -8, 301), - placeWindFarm(70, 52, 3, 2, 12, -24, -14, 3, 401), - placeWindFarm(-100, -8, 2, 2, 14, 22, 8, 0, 501), - placeWindFarm(90, -64, 3, 2, 12, -26, 10, -6, 601), - placeWindFarm(-60, 92, 2, 2, 13, 18, -16, 18, 701), - placeWindFarm(44, -84, 2, 3, 12, -18, 20, 0, 801), + placeWindFarm(-120, 90, 4, 3, 16, 40, -15, 5, 101), + placeWindFarm(25, 130, 3, 3, 15, -28, -20, 12, 201), + placeWindFarm(-75, -35, 3, 3, 14, 28, 18, -8, 301), + placeWindFarm(100, 75, 4, 2, 14, -30, -18, 3, 401), + placeWindFarm(-150, -15, 3, 2, 16, 28, 10, 0, 501), + placeWindFarm(130, -90, 3, 3, 14, -32, 15, -6, 601), + placeWindFarm(-90, 140, 3, 2, 15, 24, -20, 18, 701), + placeWindFarm(65, -120, 3, 3, 14, -22, 25, 0, 801), + placeWindFarm(-40, 60, 2, 3, 14, 20, -16, 10, 901), + placeWindFarm(170, 20, 3, 2, 15, -30, 12, -3, 1001), ]; -// Step 4: Solar parks with subs at edge +// Solar parks with substations at edge type SolarPark = { pos: THREE.Vector3; sub: THREE.Vector3; rows: number; cols: number }; const SOLAR_PARKS: SolarPark[] = [ - { pos: onTerrain(-114, -56), sub: onTerrain(-98, -48), rows: 6, cols: 8 }, - { pos: onTerrain(-104, -72), sub: onTerrain(-98, -48), rows: 5, cols: 7 }, - { pos: onTerrain(100, 18), sub: onTerrain(88, 12), rows: 5, cols: 7 }, - { pos: onTerrain(-48, -70), sub: onTerrain(-40, -58), rows: 4, cols: 6 }, - { pos: onTerrain(58, -20), sub: onTerrain(52, -14), rows: 5, cols: 6 }, - { pos: onTerrain(-84, 34), sub: onTerrain(-74, 30), rows: 4, cols: 5 }, + { pos: onTerrain(-170, -80), sub: onTerrain(-145, -70), rows: 7, cols: 10 }, + { pos: onTerrain(-155, -105), sub: onTerrain(-145, -70), rows: 6, cols: 8 }, + { pos: onTerrain(145, 28), sub: onTerrain(130, 20), rows: 6, cols: 9 }, + { pos: onTerrain(-70, -100), sub: onTerrain(-58, -85), rows: 5, cols: 7 }, + { pos: onTerrain(85, -30), sub: onTerrain(75, -20), rows: 6, cols: 8 }, + { pos: onTerrain(-125, 50), sub: onTerrain(-110, 42), rows: 5, cols: 6 }, + { pos: onTerrain(50, -170), sub: onTerrain(48, -148), rows: 5, cols: 7 }, + { pos: onTerrain(-30, -160), sub: onTerrain(-25, -138), rows: 6, cols: 8 }, ]; // Deduplicated solar subs @@ -173,13 +187,13 @@ SOLAR_PARKS.forEach(sp => { if (!solarSubSet.has(k)) { solarSubSet.add(k); SOLAR_SUBS.push(sp.sub); } }); -// Step 5: Collect all gen subs and auto-route to hubs +// Collect all generation subs and auto-route to hubs const ALL_GEN_SUBS = [...WIND_FARMS.map(f => f.sub), ...SOLAR_SUBS]; const ALL_CITY_SUBS = CITY_DATA.map(c => c.sub); const ALL_SUBS = [...ALL_GEN_SUBS, ...HUBS, ...ALL_CITY_SUBS]; -// Step 6: HSVL tower routes -const TWR_H = 9; +// Tower routing +const TWR_H = 12; function lerpRoute(from: THREE.Vector3, to: THREE.Vector3, n: number): THREE.Vector3[] { const pts = [from]; for (let i = 1; i <= n; i++) { @@ -190,7 +204,7 @@ function lerpRoute(from: THREE.Vector3, to: THREE.Vector3, n: number): THREE.Vec return pts; } function autoTowerCount(a: THREE.Vector3, b: THREE.Vector3) { - return Math.max(2, Math.floor(Math.hypot(a.x - b.x, a.z - b.z) / 22)); + return Math.max(2, Math.floor(Math.hypot(a.x - b.x, a.z - b.z) / 28)); } const HSVL_ROUTES: THREE.Vector3[][] = []; @@ -204,13 +218,13 @@ CITY_DATA.forEach(cd => { HSVL_ROUTES.push(lerpRoute(hub, cd.sub, autoTowerCount(hub, cd.sub))); }); -// Step 7: All cable pairs +// Distribution cables: solar→sub, sub→city const DIST_CABLES: [THREE.Vector3, THREE.Vector3][] = [ ...SOLAR_PARKS.map(sp => [sp.pos, sp.sub] as [THREE.Vector3, THREE.Vector3]), ...CITY_DATA.map(cd => [cd.sub, cd.city] as [THREE.Vector3, THREE.Vector3]), ]; -// Step 8: All infra for exclusion +// All infrastructure for exclusion zones const ALL_INFRA: THREE.Vector3[] = [ ...ALL_SUBS, ...CITY_DATA.map(c => c.city), @@ -218,15 +232,15 @@ const ALL_INFRA: THREE.Vector3[] = [ ...SOLAR_PARKS.map(s => s.pos), ]; -// Step 9: Poisson-disc distributed forests -const FOREST_POSITIONS = poissonDisc(55, 18, 140, 5000, ALL_INFRA, 14); +// Forest positions via Poisson disc +const FOREST_POSITIONS = poissonDisc(80, 22, 220, 5000, ALL_INFRA, 18); // ═══════════════════════════════════════════════════════════════ -// TERRAIN MESH — uses the same terrainY() function +// TERRAIN MESH // ═══════════════════════════════════════════════════════════════ const Terrain = () => { const geometry = useMemo(() => { - const geo = new THREE.CircleGeometry(TERRAIN_R, 200); + const geo = new THREE.CircleGeometry(TERRAIN_R, 256); const pos = geo.attributes.position; for (let i = 0; i < pos.count; i++) { const x = pos.getX(i); @@ -238,10 +252,34 @@ const Terrain = () => { geo.computeVertexNormals(); return geo; }, []); + return ( - - + + + + + + + {/* Subtle grid glow on terrain */} + + + ); }; @@ -250,15 +288,50 @@ const Terrain = () => { // COMPONENTS // ═══════════════════════════════════════════════════════════════ +// ── SOLAR FIELD ── const SolarField = ({ position, rows = 5, cols = 7 }: { position: THREE.Vector3; rows?: number; cols?: number }) => ( {Array.from({ length: rows }).map((_, r) => Array.from({ length: cols }).map((_, c) => ( - - - - - + + {/* Post */} + + + + + {/* Panel */} + + + + + + {/* Grid lines on panel */} + + + + + {/* Subtle blue reflection shine */} + + + + )) @@ -266,20 +339,62 @@ const SolarField = ({ position, rows = 5, cols = 7 }: { position: THREE.Vector3; ); -const TH = 6, BLade = 3; +// ── WIND TURBINE ── +const TURBINE_H = 8; +const BLADE_L = 4; + const WindTurbine = ({ position, seed = 0 }: { position: THREE.Vector3; seed?: number }) => { const bladesRef = useRef(null); - const speed = 1.8 + sr(seed * 7) * 1.2; - useFrame((s) => { if (bladesRef.current) bladesRef.current.rotation.z = s.clock.elapsedTime * speed; }); + const speed = 1.5 + sr(seed * 7) * 1.5; + useFrame((s) => { + if (bladesRef.current) bladesRef.current.rotation.z = s.clock.elapsedTime * speed; + }); + return ( - - - - + {/* Tower — tapered */} + + + + + {/* Nacelle */} + + + + + {/* Rotor hub + blades */} + + + + + {[0, 1, 2].map(i => ( - + + + + ))} @@ -287,39 +402,109 @@ const WindTurbine = ({ position, seed = 0 }: { position: THREE.Vector3; seed?: n ); }; +// ── SUBSTATION ── const Substation = ({ position, size = 'small' }: { position: THREE.Vector3; size?: 'small' | 'large' }) => { - const s = size === 'large' ? 1.3 : 0.8; + const s = size === 'large' ? 1.5 : 0.9; const pulseRef = useRef(null); - useFrame((st) => { if (pulseRef.current) pulseRef.current.scale.setScalar(1 + Math.sin(st.clock.elapsedTime * 2.5) * 0.15); }); + useFrame((st) => { + if (pulseRef.current) { + const pulse = 1 + Math.sin(st.clock.elapsedTime * 3) * 0.2; + pulseRef.current.scale.setScalar(pulse); + } + }); + return ( - - - {[[-0.8 * s, -0.5 * s], [0.8 * s, -0.5 * s], [0, 0.6 * s]].map(([x, z], i) => ( + {/* Fenced area */} + + + + + {/* Ground pad */} + + + + + {/* Transformers */} + {[[-1 * s, -0.6 * s], [1 * s, -0.6 * s], [0, 0.7 * s]].map(([x, z], i) => ( - - - + + + + + {/* Wireframe overlay */} + + + + + {/* Insulator */} + + + + ))} - - + {/* Bus bar */} + + + + + {/* Pulsing energy indicator */} + + + + ); }; +// ── CITY ── const City = ({ position, seed = 0 }: { position: THREE.Vector3; seed?: number }) => { const buildings = useMemo(() => { const items = []; - const count = 10 + Math.floor(sr(seed * 3) * 8); + const count = 14 + Math.floor(sr(seed * 3) * 10); for (let i = 0; i < count; i++) { const angle = sr(seed * 100 + i * 17) * Math.PI * 2; - const radius = 0.5 + sr(seed * 100 + i * 29) * 6; - const height = 1 + sr(seed * 100 + i * 37) * 4; + const radius = 0.8 + sr(seed * 100 + i * 29) * 8; + const height = 1.2 + sr(seed * 100 + i * 37) * 5; items.push({ - x: Math.cos(angle) * radius, z: Math.sin(angle) * radius, - height, width: 0.4 + sr(seed * 100 + i * 43) * 0.8, - depth: 0.4 + sr(seed * 100 + i * 47) * 0.6, + x: Math.cos(angle) * radius, + z: Math.sin(angle) * radius, + height, + width: 0.5 + sr(seed * 100 + i * 43) * 1, + depth: 0.5 + sr(seed * 100 + i * 47) * 0.8, rows: Math.max(1, Math.floor(height / 0.8)), }); } @@ -330,50 +515,163 @@ const City = ({ position, seed = 0 }: { position: THREE.Vector3; seed?: number } {buildings.map((b, i) => ( - + {/* Building body */} + + + + + {/* Building edge glow */} + + + + + {/* Windows — glowing */} {Array.from({ length: b.rows }).map((_, row) => { const wc = Math.max(1, Math.floor(b.width / 0.35)); return Array.from({ length: wc }).map((_, col) => { - if (sr(seed * 1000 + i * 100 + row * 10 + col) < 0.25) return null; - const color = sr(seed * 2000 + i * 50 + row + col) > 0.45 ? '#ffdd44' : '#aaddff'; + if (sr(seed * 1000 + i * 100 + row * 10 + col) < 0.2) return null; + const isWarm = sr(seed * 2000 + i * 50 + row + col) > 0.4; + const color = isWarm ? '#ffcc33' : '#88ccff'; + const intensity = 0.6 + sr(seed * 3000 + i * 30 + row * 5 + col) * 0.4; const xP = wc > 1 ? (col / (wc - 1) - 0.5) * (b.width * 0.7) : 0; return ( - - + + + + + + + + ); }); })} - + {/* Rooftop light */} + + + + ))} - + {/* City ambient glow */} + + ); }; // ═══════════════════════════════════════════════════════════════ -// HOCHSPANNUNGSMAST — tapered pylon +// TRANSMISSION TOWER — lattice-style pylon // ═══════════════════════════════════════════════════════════════ const TransmissionTower = ({ position }: { position: THREE.Vector3 }) => ( - - - - - {[-1.6, 1.6].map((xOff, i) => ( - + {/* Main tower body — tapered */} + + + + + {/* Wireframe lattice overlay */} + + + + + {/* Top cross-arm */} + + + + + {/* Middle cross-arm */} + + + + + {/* Insulator tips */} + {[-2, -0.8, 0.8, 2].map((xOff, i) => ( + + + + ))} - + {/* Aviation light */} + + + + ); // ═══════════════════════════════════════════════════════════════ -// CABLE NETWORK — all use catenary algorithm +// CABLE NETWORK // ═══════════════════════════════════════════════════════════════ const CableNetwork = () => { - // Pre-compute all cable point arrays const cables = useMemo(() => { const result: { points: THREE.Vector3[]; color: string; width: number; opacity: number }[] = []; @@ -383,31 +681,31 @@ const CableNetwork = () => { const a = route[i - 1].clone(); a.y += TWR_H; const b = route[i].clone(); b.y += TWR_H; result.push({ - points: catenaryPoints(a, b, 2.5, 20), - color: '#6699cc', width: 1.5, opacity: 0.4, + points: catenaryPoints(a, b, 3, 24), + color: '#5599cc', width: 1.5, opacity: 0.35, }); } }); - // Distribution cables: solar→sub, sub→city (elevated) + // Distribution cables DIST_CABLES.forEach(([from, to]) => { - const a = from.clone(); a.y += 5; - const b = to.clone(); b.y += 5; + const a = from.clone(); a.y += 6; + const b = to.clone(); b.y += 6; result.push({ - points: catenaryPoints(a, b, 1.5, 16), - color: '#55cc88', width: 1.2, opacity: 0.35, + points: catenaryPoints(a, b, 2, 18), + color: '#44bb88', width: 1.2, opacity: 0.3, }); }); - // Wind farm internal chains: ground-level cables + // Wind farm internal chains WIND_FARMS.forEach(farm => { farm.chains.forEach(chain => { for (let i = 1; i < chain.length; i++) { - const a = chain[i - 1].clone(); a.y += 0.3; - const b = chain[i].clone(); b.y += 0.3; + const a = chain[i - 1].clone(); a.y += 0.4; + const b = chain[i].clone(); b.y += 0.4; result.push({ - points: catenaryPoints(a, b, 0.3, 8), - color: '#30ff70', width: 1, opacity: 0.2, + points: catenaryPoints(a, b, 0.4, 10), + color: '#30ee70', width: 0.8, opacity: 0.18, }); } }); @@ -416,7 +714,6 @@ const CableNetwork = () => { return result; }, []); - // Towers at intermediate route points const towers = useMemo(() => { const pts: THREE.Vector3[] = []; HSVL_ROUTES.forEach(route => { @@ -429,38 +726,44 @@ const CableNetwork = () => { {towers.map((pos, i) => )} {cables.map((c, i) => ( - + ))} ); }; // ═══════════════════════════════════════════════════════════════ -// FOREST — Poisson-disc distributed, terrain-aware +// FOREST // ═══════════════════════════════════════════════════════════════ const Forest = ({ position, seed = 0, count = 30 }: { position: THREE.Vector3; seed?: number; count?: number }) => { const trees = useMemo(() => { const items = []; for (let i = 0; i < count; i++) { - // Use noise for natural clumping const angle = sr(seed * 200 + i * 11) * Math.PI * 2; - const radius = sr(seed * 200 + i * 23) * 14; + const radius = sr(seed * 200 + i * 23) * 18; const wx = position.x + Math.cos(angle) * radius; const wz = position.z + Math.sin(angle) * radius; - // Skip if near infrastructure - if (ALL_INFRA.some(p => Math.hypot(wx - p.x, wz - p.z) < 10)) continue; - // Skip if too far from terrain center - if (Math.sqrt(wx * wx + wz * wz) > TERRAIN_R * 0.72) continue; + if (ALL_INFRA.some(p => Math.hypot(wx - p.x, wz - p.z) < 12)) continue; + if (Math.sqrt(wx * wx + wz * wz) > TERRAIN_R * 0.68) continue; - const localY = terrainY(wx, wz) - position.y; // height relative to parent group - const trunkH = 0.4 + sr(seed * 200 + i * 31) * 1; - const canopyH = 0.6 + sr(seed * 200 + i * 37) * 1.5; - const canopyR = 0.2 + sr(seed * 200 + i * 43) * 0.4; + const localY = terrainY(wx, wz) - position.y; + const trunkH = 0.5 + sr(seed * 200 + i * 31) * 1.2; + const canopyH = 0.8 + sr(seed * 200 + i * 37) * 2; + const canopyR = 0.25 + sr(seed * 200 + i * 43) * 0.5; const shade = sr(seed * 200 + i * 47); items.push({ - x: Math.cos(angle) * radius, y: localY, z: Math.sin(angle) * radius, + x: Math.cos(angle) * radius, + y: localY, + z: Math.sin(angle) * radius, trunkH, canopyH, canopyR, shade, - brightness: 0.2 + sr(seed * 200 + i * 41) * 0.3, + brightness: 0.15 + sr(seed * 200 + i * 41) * 0.25, }); } return items; @@ -470,9 +773,35 @@ const Forest = ({ position, seed = 0, count = 30 }: { position: THREE.Vector3; s {trees.map((t, i) => ( - - 0.6 ? '#15aa45' : t.shade > 0.3 ? '#1a9050' : '#10804a'} transparent opacity={t.brightness} toneMapped={false} /> - 0.5 ? '#20cc55' : '#18bb50'} transparent opacity={t.brightness * 0.7} toneMapped={false} /> + {/* Trunk */} + + + + + {/* Lower canopy */} + + + 0.6 ? '#12aa40' : t.shade > 0.3 ? '#159050' : '#0d7545'} + transparent + opacity={t.brightness} + toneMapped={false} + /> + + {/* Upper canopy layer */} + + + 0.5 ? '#1cc855' : '#15b048'} + transparent + opacity={t.brightness * 0.6} + toneMapped={false} + /> + ))} @@ -480,13 +809,12 @@ const Forest = ({ position, seed = 0, count = 30 }: { position: THREE.Vector3; s }; // ═══════════════════════════════════════════════════════════════ -// ENERGY PARTICLES — flow along catenary cables +// ENERGY PARTICLES — flow along all cables // ═══════════════════════════════════════════════════════════════ const EnergyParticles = () => { const meshRef = useRef(null); const dummy = useMemo(() => new THREE.Object3D(), []); - // Build all curves from cable segments for particle paths const curves = useMemo(() => { const all: THREE.CatmullRomCurve3[] = []; // HSVL @@ -494,45 +822,52 @@ const EnergyParticles = () => { for (let i = 1; i < route.length; i++) { const a = route[i - 1].clone(); a.y += TWR_H; const b = route[i].clone(); b.y += TWR_H; - all.push(new THREE.CatmullRomCurve3(catenaryPoints(a, b, 2.5, 12))); + all.push(new THREE.CatmullRomCurve3(catenaryPoints(a, b, 3, 14))); } }); // Distribution DIST_CABLES.forEach(([from, to]) => { - const a = from.clone(); a.y += 5; - const b = to.clone(); b.y += 5; - all.push(new THREE.CatmullRomCurve3(catenaryPoints(a, b, 1.5, 10))); + const a = from.clone(); a.y += 6; + const b = to.clone(); b.y += 6; + all.push(new THREE.CatmullRomCurve3(catenaryPoints(a, b, 2, 12))); }); // Farm chains WIND_FARMS.forEach(farm => { farm.chains.forEach(chain => { for (let i = 1; i < chain.length; i++) { - const a = chain[i - 1].clone(); a.y += 0.3; - const b = chain[i].clone(); b.y += 0.3; - all.push(new THREE.CatmullRomCurve3(catenaryPoints(a, b, 0.3, 6))); + const a = chain[i - 1].clone(); a.y += 0.4; + const b = chain[i].clone(); b.y += 0.4; + all.push(new THREE.CatmullRomCurve3(catenaryPoints(a, b, 0.4, 8))); } }); }); return all; }, []); - const perCurve = 4; + const perCurve = 5; const total = curves.length * perCurve; const data = useMemo(() => { const items = []; for (let c = 0; c < curves.length; c++) for (let p = 0; p < perCurve; p++) - items.push({ curve: c, t: p / perCurve, speed: 0.3 + sr(c * 100 + p * 7) * 0.35, phase: sr(c * 100 + p * 13) * Math.PI * 2 }); + items.push({ + curve: c, + t: p / perCurve, + speed: 0.25 + sr(c * 100 + p * 7) * 0.4, + phase: sr(c * 100 + p * 13) * Math.PI * 2, + }); return items; }, [curves]); useFrame((state, delta) => { if (!meshRef.current) return; data.forEach((d, i) => { - d.t += delta * d.speed; if (d.t > 1) d.t -= 1; + d.t += delta * d.speed; + if (d.t > 1) d.t -= 1; const p = curves[d.curve].getPointAt(d.t); dummy.position.copy(p); - dummy.scale.setScalar(0.5 + Math.sin(state.clock.elapsedTime * 6 + d.phase) * 0.3); + const pulse = 0.4 + Math.sin(state.clock.elapsedTime * 8 + d.phase) * 0.35; + dummy.scale.setScalar(pulse); dummy.updateMatrix(); meshRef.current!.setMatrixAt(i, dummy.matrix); }); @@ -541,37 +876,105 @@ const EnergyParticles = () => { return ( - - + + ); }; // ═══════════════════════════════════════════════════════════════ -// AMBIENT MOTES +// AMBIENT ENERGY MOTES // ═══════════════════════════════════════════════════════════════ const AmbientMotes = () => { const meshRef = useRef(null); - const count = 250; + const count = 350; const dummy = useMemo(() => new THREE.Object3D(), []); - const motes = useMemo(() => Array.from({ length: count }, (_, i) => ({ - x: (sr(i * 7 + 1) - 0.5) * 220, y: 3 + sr(i * 13 + 2) * 18, z: (sr(i * 19 + 3) - 0.5) * 220, - speed: 0.1 + sr(i * 23 + 4) * 0.3, phase: sr(i * 29 + 5) * Math.PI * 2, - })), []); + const motes = useMemo(() => + Array.from({ length: count }, (_, i) => ({ + x: (sr(i * 7 + 1) - 0.5) * 320, + y: 4 + sr(i * 13 + 2) * 25, + z: (sr(i * 19 + 3) - 0.5) * 320, + speed: 0.08 + sr(i * 23 + 4) * 0.25, + phase: sr(i * 29 + 5) * Math.PI * 2, + })), []); useFrame((state) => { if (!meshRef.current) return; motes.forEach((m, i) => { - dummy.position.set(m.x + Math.sin(state.clock.elapsedTime * m.speed + m.phase) * 3, m.y + Math.sin(state.clock.elapsedTime * m.speed * 0.7 + m.phase) * 2, m.z + Math.cos(state.clock.elapsedTime * m.speed + m.phase) * 3); - dummy.scale.setScalar(0.3 + Math.sin(state.clock.elapsedTime * 2 + m.phase) * 0.2); - dummy.updateMatrix(); meshRef.current!.setMatrixAt(i, dummy.matrix); + dummy.position.set( + m.x + Math.sin(state.clock.elapsedTime * m.speed + m.phase) * 4, + m.y + Math.sin(state.clock.elapsedTime * m.speed * 0.6 + m.phase) * 3, + m.z + Math.cos(state.clock.elapsedTime * m.speed + m.phase) * 4, + ); + const s = 0.25 + Math.sin(state.clock.elapsedTime * 2.5 + m.phase) * 0.2; + dummy.scale.setScalar(s); + dummy.updateMatrix(); + meshRef.current!.setMatrixAt(i, dummy.matrix); }); meshRef.current.instanceMatrix.needsUpdate = true; }); return ( - + + + + ); +}; + +// ═══════════════════════════════════════════════════════════════ +// GROUND FOG PARTICLES — atmospheric haze near terrain +// ═══════════════════════════════════════════════════════════════ +const GroundFog = () => { + const meshRef = useRef(null); + const count = 120; + const dummy = useMemo(() => new THREE.Object3D(), []); + const particles = useMemo(() => + Array.from({ length: count }, (_, i) => { + const x = (sr(i * 31 + 100) - 0.5) * TERRAIN_R * 1.6; + const z = (sr(i * 37 + 200) - 0.5) * TERRAIN_R * 1.6; + return { + x, + y: terrainY(x, z) + 0.5 + sr(i * 43 + 300) * 3, + z, + scale: 8 + sr(i * 47 + 400) * 15, + speed: 0.02 + sr(i * 53 + 500) * 0.04, + phase: sr(i * 59 + 600) * Math.PI * 2, + }; + }), []); + + useFrame((state) => { + if (!meshRef.current) return; + particles.forEach((p, i) => { + dummy.position.set( + p.x + Math.sin(state.clock.elapsedTime * p.speed + p.phase) * 5, + p.y, + p.z + Math.cos(state.clock.elapsedTime * p.speed + p.phase) * 5, + ); + dummy.scale.setScalar(p.scale); + dummy.updateMatrix(); + meshRef.current!.setMatrixAt(i, dummy.matrix); + }); + meshRef.current.instanceMatrix.needsUpdate = true; + }); + + return ( + + + ); }; @@ -581,23 +984,51 @@ const AmbientMotes = () => { // ═══════════════════════════════════════════════════════════════ const Scene = () => ( <> - - - - - - - - + + + + {/* Lighting */} + + + + + + + + {/* Starfield */} + + + {/* Atmospheric fog */} + + {/* Solar parks */} - {SOLAR_PARKS.map((sp, i) => )} + {SOLAR_PARKS.map((sp, i) => ( + + ))} {/* Wind farms */} {WIND_FARMS.flatMap((farm, fi) => - farm.turbines.map((pos, ti) => ) + farm.turbines.map((pos, ti) => ( + + )) )} {/* Substations */} @@ -609,20 +1040,31 @@ const Scene = () => ( {/* Cities */} {CITY_DATA.map((cd, i) => )} - {/* All cables with catenary physics */} + {/* Cable network with towers */} - {/* Forests — Poisson-disc placed */} + {/* Forests */} {FOREST_POSITIONS.map((pos, i) => ( - + ))} {/* Energy flow */} + {/* Post-processing */} - + ); @@ -632,12 +1074,38 @@ export default function HeroIllustration() { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect if (!mounted) return null; + return ( -
- +
+ -
+ {/* Cinematic vignette overlay */} +
+ {/* Bottom fade into page */} +
); } diff --git a/docker-compose.yml b/docker-compose.yml index 826d160a..e2f388a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,7 @@ services: - "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}" # Public Router – paths that bypass Gatekeeper auth (health, SEO, static assets, OG images) - - "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/(health|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)" + - "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)" - "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}" - "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}" - "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}" @@ -46,6 +46,18 @@ services: - "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Proto=https" - "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on" + # Login redirect – the app's middleware sends users to /login but login lives at /gatekeeper/login + - "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.regex=^https?://[^/]+/([a-z]{2}/)?login(.*)" + - "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.replacement=https://${TRAEFIK_HOST:-klz-cables.com}/gatekeeper/login$${2}" + - "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.permanent=false" + - "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/([a-z]{2}/)?login`)" + - "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.entrypoints=${TRAEFIK_ENTRYPOINT:-web}" + - "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}" + - "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.tls=${TRAEFIK_TLS:-false}" + - "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.middlewares=${PROJECT_NAME:-klz}-loginredirect" + - "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.service=${PROJECT_NAME:-klz}-app-svc" + - "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.priority=2002" + klz-gatekeeper: profiles: [ "gatekeeper" ] image: registry.infra.mintel.me/mintel/gatekeeper:testing @@ -66,8 +78,8 @@ services: - "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.authResponseHeaders=X-Auth-User" - "traefik.docker.network=infra" - # Gatekeeper Public Router (Login/Auth UI) - - "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.rule=Host(`${GATEKEEPER_HOST:-gatekeeper.klz-cables.com}`)" + # Gatekeeper Public Router (Login/Auth UI) — basePath mode on main domain + - "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathPrefix(`/gatekeeper`)" - "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}" - "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}" - "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls=${TRAEFIK_TLS:-false}"