From 8e2a06d6f2fc5826c1d5e57394e5708a599ce5c6 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 27 Feb 2026 02:10:17 +0100 Subject: [PATCH] fix: revert hero --- components/home/Hero.tsx | 20 +- components/home/HeroIllustration.tsx | 1487 +++++++------------------- 2 files changed, 410 insertions(+), 1097 deletions(-) diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx index b824dd01..a3408da5 100644 --- a/components/home/Hero.tsx +++ b/components/home/Hero.tsx @@ -23,12 +23,26 @@ export default function Hero({ data }: { data?: any }) { className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]" > {data?.title ? ( - /g, '').replace(/<\/green>/g, '') }} /> + /g, + '', + ) + .replace( + /<\/green>/g, + '', + ), + }} + /> ) : ( t.rich('title', { green: (chunks) => ( - {chunks} + + {chunks} +
-
+
diff --git a/components/home/HeroIllustration.tsx b/components/home/HeroIllustration.tsx index f529edd5..1b065bcb 100644 --- a/components/home/HeroIllustration.tsx +++ b/components/home/HeroIllustration.tsx @@ -1,1111 +1,410 @@ -/* eslint-disable react/no-unknown-property */ 'use client'; -import React, { useRef, useMemo, useState, useEffect } from 'react'; -import { Canvas, useFrame } from '@react-three/fiber'; -import { OrbitControls, PerspectiveCamera, Stars, Line } from '@react-three/drei'; -import { EffectComposer, Bloom } from '@react-three/postprocessing'; -import * as THREE from 'three'; +import React, { useEffect, useState } from 'react'; -// ═══════════════════════════════════════════════════════════════ -// CORE ALGORITHMS -// ═══════════════════════════════════════════════════════════════ +// Isometric grid configuration - true 2:1 isometric projection +const CELL_WIDTH = 120; +const CELL_HEIGHT = 60; // Half of width for 2:1 isometric -// Deterministic hash -function sr(seed: number) { - const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453; - return x - Math.floor(x); +// Convert grid coordinates to isometric screen coordinates +function gridToScreen(col: number, row: number): { x: number; y: number } { + return { + x: (col - row) * (CELL_WIDTH / 2), + y: (col + row) * (CELL_HEIGHT / 2), + }; } -// ── TERRAIN HEIGHT FUNCTION ── -// SINGLE SOURCE OF TRUTH for terrain elevation. -const TERRAIN_R = 400; -const TERRAIN_CURVATURE = 0.00012; // planetary curvature +// Grid layout (10 columns x 8 rows) +const GRID = { + cols: 10, + rows: 8, +}; -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.2); +// Infrastructure positions +const INFRASTRUCTURE = { + solar: [ + { col: 0, row: 5 }, + { col: 1, row: 5 }, + { col: 0, row: 6 }, + { col: 1, row: 6 }, + { col: 2, row: 7 }, + { col: 3, row: 7 }, + { col: 2, row: 8 }, + { col: 3, row: 8 }, + ], + wind: [ + { col: 0, row: 1 }, + { col: 1, row: 2 }, + { col: 2, row: 1 }, + { col: 3, row: 0 }, + { col: 4, row: 1 }, + { col: 5, row: 0 }, + ], + substations: [ + { col: 3, row: 3, type: 'collection' }, + { col: 6, row: 4, type: 'distribution' }, + { col: 5, row: 7, type: 'distribution' }, + ], + towers: [ + { col: 4, row: 3 }, + { col: 5, row: 4 }, + { col: 4, row: 5 }, + { col: 5, row: 6 }, + ], + city: [ + { col: 8, row: 3, type: 'tall' }, + { col: 9, row: 4, type: 'medium' }, + { col: 8, row: 5, type: 'small' }, + { col: 9, row: 5, type: 'medium' }, + ], + city2: [ + { col: 6, row: 8, type: 'medium' }, + { col: 7, row: 7, type: 'tall' }, + { col: 7, row: 8, type: 'small' }, + ], + trees: [ + { col: 0, row: 3 }, + { col: 2, row: 6 }, + { col: 3, row: 1 }, + { col: 6, row: 2 }, + { col: 6, row: 6 }, + ], +}; - // 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.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; -} - -function onTerrain(x: number, z: number): THREE.Vector3 { - return new THREE.Vector3(x, terrainY(x, z), z); -} - -// ── CATENARY CABLE ── -function catenarySag(t: number): number { - const u = t * 2 - 1; - const cEdge = Math.cosh(2.5); - return -(cEdge - Math.cosh(u * 2.5)) / (cEdge - 1); -} - -function catenaryPoints( - p1: THREE.Vector3, p2: THREE.Vector3, - sag: number, segments: number = 20, -): THREE.Vector3[] { - const pts: THREE.Vector3[] = []; - for (let i = 0; i <= segments; i++) { - const t = i / segments; - pts.push(new THREE.Vector3( - p1.x + (p2.x - p1.x) * t, - p1.y + (p2.y - p1.y) * t + catenarySag(t) * sag, - p1.z + (p2.z - p1.z) * t, - )); - } - return pts; -} - -// ── POISSON DISC SAMPLING ── -function poissonDisc( - count: number, radius: number, bounds: number, seed: number, - exclusions: THREE.Vector3[] = [], exRadius = 18, -): THREE.Vector3[] { - const pts: THREE.Vector3[] = []; - 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.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)); - } - return pts; -} - -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; } - } - return best; -} - -// ═══════════════════════════════════════════════════════════════ -// PROCEDURAL WORLD -// ═══════════════════════════════════════════════════════════════ - -// Hub substations — central distribution points -const HUBS = [onTerrain(-10, 20), onTerrain(60, -5)]; - -// Cities at the periphery with their substations -const CITY_DATA = [ - { 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) }, +const POWER_LINES = [ + { from: { col: 0, row: 1 }, to: { col: 1, row: 1 } }, + { from: { col: 1, row: 2 }, to: { col: 1, row: 1 } }, + { from: { col: 2, row: 1 }, to: { col: 1, row: 1 } }, + { from: { col: 1, row: 1 }, to: { col: 1, row: 3 } }, + { from: { col: 1, row: 3 }, to: { col: 3, row: 3 } }, + { from: { col: 3, row: 0 }, to: { col: 4, row: 0 } }, + { from: { col: 4, row: 0 }, to: { col: 4, row: 1 } }, + { from: { col: 5, row: 0 }, to: { col: 5, row: 1 } }, + { from: { col: 5, row: 1 }, to: { col: 4, row: 1 } }, + { from: { col: 4, row: 1 }, to: { col: 4, row: 3 } }, + { from: { col: 4, row: 3 }, to: { col: 3, row: 3 } }, + { from: { col: 0, row: 5 }, to: { col: 1, row: 5 } }, + { from: { col: 0, row: 6 }, to: { col: 0, row: 5 } }, + { from: { col: 1, row: 6 }, to: { col: 1, row: 5 } }, + { from: { col: 1, row: 5 }, to: { col: 1, row: 3 } }, + { from: { col: 2, row: 7 }, to: { col: 3, row: 7 } }, + { from: { col: 2, row: 8 }, to: { col: 2, row: 7 } }, + { from: { col: 3, row: 8 }, to: { col: 3, row: 7 } }, + { from: { col: 3, row: 7 }, to: { col: 3, row: 5 } }, + { from: { col: 3, row: 5 }, to: { col: 3, row: 3 } }, + { from: { col: 3, row: 3 }, to: { col: 4, row: 3 } }, + { from: { col: 4, row: 3 }, to: { col: 5, row: 3 } }, + { from: { col: 5, row: 3 }, to: { col: 5, row: 4 } }, + { from: { col: 5, row: 4 }, to: { col: 6, row: 4 } }, + { from: { col: 6, row: 4 }, to: { col: 7, row: 4 } }, + { from: { col: 7, row: 4 }, to: { col: 8, row: 4 } }, + { from: { col: 8, row: 4 }, to: { col: 8, row: 3 } }, + { from: { col: 8, row: 4 }, to: { col: 8, row: 5 } }, + { from: { col: 8, row: 3 }, to: { col: 9, row: 3 } }, + { from: { col: 9, row: 3 }, to: { col: 9, row: 4 } }, + { from: { col: 8, row: 5 }, to: { col: 9, row: 5 } }, + { from: { col: 3, row: 3 }, to: { col: 3, row: 5 } }, + { from: { col: 3, row: 5 }, to: { col: 4, row: 5 } }, + { from: { col: 4, row: 5 }, to: { col: 5, row: 5 } }, + { from: { col: 5, row: 5 }, to: { col: 5, row: 6 } }, + { from: { col: 5, row: 6 }, to: { col: 5, row: 7 } }, + { from: { col: 5, row: 7 }, to: { col: 6, row: 7 } }, + { from: { col: 6, row: 7 }, to: { col: 6, row: 8 } }, + { from: { col: 6, row: 7 }, to: { col: 7, row: 7 } }, + { from: { col: 7, row: 7 }, to: { col: 7, row: 8 } }, ]; -// Wind farm generation -type WindFarm = { - sub: THREE.Vector3; - turbines: THREE.Vector3[]; - chains: THREE.Vector3[][]; -}; - -function placeWindFarm( - cx: number, cz: number, - cols: number, rows: number, - spacing: number, - subOffX: number, subOffZ: number, - rotDeg: number, seed: number, -): WindFarm { - const rad = (rotDeg * Math.PI) / 180; - const co = Math.cos(rad), si = Math.sin(rad); - const grid: THREE.Vector3[][] = []; - const turbines: THREE.Vector3[] = []; - - 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) * 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); - turbines.push(pos); - row.push(pos); - } - grid.push(row); - } - - const sub = onTerrain(cx + subOffX, cz + subOffZ); - const chains: THREE.Vector3[][] = grid.map(row => [...row, sub]); - - return { sub, turbines, chains }; -} - -const WIND_FARMS: WindFarm[] = [ - 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), -]; - -// Solar parks with substations at edge -type SolarPark = { pos: THREE.Vector3; sub: THREE.Vector3; rows: number; cols: number }; - -const SOLAR_PARKS: SolarPark[] = [ - { 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 -const solarSubSet = new Set(); -const SOLAR_SUBS: THREE.Vector3[] = []; -SOLAR_PARKS.forEach(sp => { - const k = `${sp.sub.x.toFixed(1)},${sp.sub.z.toFixed(1)}`; - if (!solarSubSet.has(k)) { solarSubSet.add(k); SOLAR_SUBS.push(sp.sub); } -}); - -// 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]; - -// 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++) { - const t = i / (n + 1); - pts.push(onTerrain(from.x + (to.x - from.x) * t, from.z + (to.z - from.z) * t)); - } - pts.push(to); - 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) / 28)); -} - -const HSVL_ROUTES: THREE.Vector3[][] = []; -ALL_GEN_SUBS.forEach(sub => { - const hub = nearest(sub, HUBS); - HSVL_ROUTES.push(lerpRoute(sub, hub, autoTowerCount(sub, hub))); -}); -HSVL_ROUTES.push(lerpRoute(HUBS[0], HUBS[1], autoTowerCount(HUBS[0], HUBS[1]))); -CITY_DATA.forEach(cd => { - const hub = nearest(cd.sub, HUBS); - HSVL_ROUTES.push(lerpRoute(hub, cd.sub, autoTowerCount(hub, cd.sub))); -}); - -// 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]), -]; - -// All infrastructure for exclusion zones -const ALL_INFRA: THREE.Vector3[] = [ - ...ALL_SUBS, - ...CITY_DATA.map(c => c.city), - ...WIND_FARMS.flatMap(f => f.turbines), - ...SOLAR_PARKS.map(s => s.pos), -]; - -// Forest positions via Poisson disc -const FOREST_POSITIONS = poissonDisc(80, 22, 220, 5000, ALL_INFRA, 18); - -// ═══════════════════════════════════════════════════════════════ -// TERRAIN MESH -// ═══════════════════════════════════════════════════════════════ -const Terrain = () => { - const geometry = useMemo(() => { - 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); - const z = -pos.getY(i); - pos.setX(i, x); - pos.setY(i, terrainY(x, z)); - pos.setZ(i, z); - } - geo.computeVertexNormals(); - return geo; - }, []); - - return ( - - - - - - - - {/* Subtle grid glow on 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 */} - - - - - - - )) - )} - -); - -// ── 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.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 => ( - - - - - - - ))} - - - ); -}; - -// ── SUBSTATION ── -const Substation = ({ position, size = 'small' }: { position: THREE.Vector3; size?: 'small' | 'large' }) => { - const s = size === 'large' ? 1.5 : 0.9; - const pulseRef = useRef(null); - useFrame((st) => { - if (pulseRef.current) { - const pulse = 1 + Math.sin(st.clock.elapsedTime * 3) * 0.2; - pulseRef.current.scale.setScalar(pulse); - } - }); - - return ( - - {/* 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 = 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.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.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)), - }); - } - return items; - }, [seed]); - - return ( - - {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.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 */} - - - - ); -}; - -// ═══════════════════════════════════════════════════════════════ -// TRANSMISSION TOWER — lattice-style pylon -// ═══════════════════════════════════════════════════════════════ -const TransmissionTower = ({ position }: { position: THREE.Vector3 }) => ( - - {/* 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 -// ═══════════════════════════════════════════════════════════════ -const CableNetwork = () => { - const cables = useMemo(() => { - const result: { points: THREE.Vector3[]; color: string; width: number; opacity: number }[] = []; - - // HSVL cables: tower-top to tower-top with catenary sag - HSVL_ROUTES.forEach(route => { - 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; - result.push({ - points: catenaryPoints(a, b, 3, 24), - color: '#5599cc', width: 1.5, opacity: 0.35, - }); - } - }); - - // Distribution cables - DIST_CABLES.forEach(([from, to]) => { - const a = from.clone(); a.y += 6; - const b = to.clone(); b.y += 6; - result.push({ - points: catenaryPoints(a, b, 2, 18), - color: '#44bb88', width: 1.2, opacity: 0.3, - }); - }); - - // 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.4; - const b = chain[i].clone(); b.y += 0.4; - result.push({ - points: catenaryPoints(a, b, 0.4, 10), - color: '#30ee70', width: 0.8, opacity: 0.18, - }); - } - }); - }); - - return result; - }, []); - - const towers = useMemo(() => { - const pts: THREE.Vector3[] = []; - HSVL_ROUTES.forEach(route => { - for (let i = 1; i < route.length - 1; i++) pts.push(route[i]); - }); - return pts; - }, []); - - return ( - - {towers.map((pos, i) => )} - {cables.map((c, i) => ( - - ))} - - ); -}; - -// ═══════════════════════════════════════════════════════════════ -// 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++) { - const angle = sr(seed * 200 + i * 11) * Math.PI * 2; - const radius = sr(seed * 200 + i * 23) * 18; - const wx = position.x + Math.cos(angle) * radius; - const wz = position.z + Math.sin(angle) * radius; - 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; - 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, - trunkH, canopyH, canopyR, shade, - brightness: 0.15 + sr(seed * 200 + i * 41) * 0.25, - }); - } - return items; - }, [seed, count, position]); - - return ( - - {trees.map((t, i) => ( - - {/* 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} - /> - - - ))} - - ); -}; - -// ═══════════════════════════════════════════════════════════════ -// ENERGY PARTICLES — flow along all cables -// ═══════════════════════════════════════════════════════════════ -const EnergyParticles = () => { - const meshRef = useRef(null); - const dummy = useMemo(() => new THREE.Object3D(), []); - - const curves = useMemo(() => { - const all: THREE.CatmullRomCurve3[] = []; - // HSVL - HSVL_ROUTES.forEach(route => { - 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, 3, 14))); - } - }); - // Distribution - DIST_CABLES.forEach(([from, to]) => { - 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.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 = 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.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; - const p = curves[d.curve].getPointAt(d.t); - dummy.position.copy(p); - 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); - }); - meshRef.current.instanceMatrix.needsUpdate = true; - }); - - return ( - - - - - ); -}; - -// ═══════════════════════════════════════════════════════════════ -// AMBIENT ENERGY MOTES -// ═══════════════════════════════════════════════════════════════ -const AmbientMotes = () => { - const meshRef = useRef(null); - const count = 350; - const dummy = useMemo(() => new THREE.Object3D(), []); - 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) * 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 ( - - - - - ); -}; - -// ═══════════════════════════════════════════════════════════════ -// SCENE -// ═══════════════════════════════════════════════════════════════ -const Scene = () => ( - <> - - - - {/* Lighting */} - - - - - - - - {/* Starfield */} - - - {/* Atmospheric fog */} - - - - - - {/* Solar parks */} - {SOLAR_PARKS.map((sp, i) => ( - - ))} - - {/* Wind farms */} - {WIND_FARMS.flatMap((farm, fi) => - farm.turbines.map((pos, ti) => ( - - )) - )} - - {/* Substations */} - {WIND_FARMS.map((f, i) => )} - {SOLAR_SUBS.map((s, i) => )} - {HUBS.map((h, i) => )} - {CITY_DATA.map((cd, i) => )} - - {/* Cities */} - {CITY_DATA.map((cd, i) => )} - - {/* Cable network with towers */} - - - {/* Forests */} - {FOREST_POSITIONS.map((pos, i) => ( - - ))} - - {/* Energy flow */} - - - - {/* Post-processing */} - - - - -); - -// ═══════════════════════════════════════════════════════════════ 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; + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth < 768); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + const viewBox = isMobile ? '400 0 1000 1100' : '-400 -200 1800 1100'; + // Increase scale slightly and opacity significantly on mobile to fix the "thin" appearance + const scale = isMobile ? 1.6 : 1; + const opacity = isMobile ? 0.9 : 0.85; return ( -
- + - {/* Cinematic vignette overlay */} -
- {/* Bottom fade into page */} -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* ISOMETRIC GRID */} + + {[...Array(GRID.rows + 1)].map((_, row) => { + const start = gridToScreen(0, row); + const end = gridToScreen(GRID.cols, row); + return ( + + ); + })} + {[...Array(GRID.cols + 1)].map((_, col) => { + const start = gridToScreen(col, 0); + const end = gridToScreen(col, GRID.rows); + return ( + + ); + })} + + + {/* POWER LINES */} + + {POWER_LINES.map((line, i) => { + const from = gridToScreen(line.from.col, line.from.row); + const to = gridToScreen(line.to.col, line.to.row); + return ; + })} + + + {/* ANIMATED ENERGY FLOW */} + + {POWER_LINES.map((line, i) => { + // Only animate a subset of lines to reduce main-thread work + if (i % 2 !== 0) return null; + const from = gridToScreen(line.from.col, line.from.row); + const to = gridToScreen(line.to.col, line.to.row); + const length = Math.sqrt(Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2)); + return ( + + + + ); + })} + + + {/* SOLAR PANELS */} + {INFRASTRUCTURE.solar.map((panel, i) => { + const pos = gridToScreen(panel.col, panel.row); + return ( + + + + + + + + ); + })} + + {/* WIND TURBINES */} + {INFRASTRUCTURE.wind.map((turbine, i) => { + const pos = gridToScreen(turbine.col, turbine.row); + return ( + + + + {[0, 120, 240].map((angle, j) => ( + + + + ))} + + + ); + })} + + {/* SUBSTATIONS */} + {INFRASTRUCTURE.substations.map((sub, i) => { + const pos = gridToScreen(sub.col, sub.row); + const isCollection = sub.type === 'collection'; + return ( + + + + + ); + })} + + {/* TRANSMISSION TOWERS */} + {INFRASTRUCTURE.towers.map((tower, i) => { + const pos = gridToScreen(tower.col, tower.row); + return ( + + + + + ); + })} + + {/* CITY BUILDINGS */} + {[...INFRASTRUCTURE.city, ...INFRASTRUCTURE.city2].map((building, i) => { + const pos = gridToScreen(building.col, building.row); + const heights = { tall: 70, medium: 45, small: 30 }; + const height = heights[building.type as keyof typeof heights] || 45; + return ( + + + + + ); + })} + + +
); }