diff --git a/components/home/HeroIllustration.tsx b/components/home/HeroIllustration.tsx
index 1b065bcb..e4b687ee 100644
--- a/components/home/HeroIllustration.tsx
+++ b/components/home/HeroIllustration.tsx
@@ -1,410 +1,8 @@
'use client';
-import React, { useEffect, useState } from 'react';
-
-// Isometric grid configuration - true 2:1 isometric projection
-const CELL_WIDTH = 120;
-const CELL_HEIGHT = 60; // Half of width for 2:1 isometric
-
-// 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),
- };
-}
-
-// Grid layout (10 columns x 8 rows)
-const GRID = {
- cols: 10,
- rows: 8,
-};
-
-// 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 },
- ],
-};
-
-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 } },
-];
+import React from 'react';
+import HeroWebGLScene from './hero-webgl/HeroWebGLScene';
export default function HeroIllustration() {
- 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 (
-
-
-
-
- );
+ return ;
}
diff --git a/components/home/hero-webgl/Generator.ts b/components/home/hero-webgl/Generator.ts
new file mode 100644
index 00000000..ac2c8754
--- /dev/null
+++ b/components/home/hero-webgl/Generator.ts
@@ -0,0 +1,307 @@
+import { createNoise2D } from 'simplex-noise';
+import { getTerrainHeight, generateClusteredPoints, generateCatenaryCurve, Vec2, Vec3, distance2D, distance3D } from './math';
+
+export type NodeType = 'wind' | 'solar' | 'city_building' | 'substation' | 'tower' | 'tree';
+
+export interface SceneNode {
+ id: string;
+ type: NodeType;
+ position: Vec3;
+ rotation: Vec3;
+ scale: Vec3;
+ clusterId?: string;
+ subType?: 'tall' | 'medium' | 'small' | string;
+}
+
+export interface SceneEdge {
+ id: string;
+ source: string;
+ target: string;
+ type: 'underground' | 'transmission'; // transmission flies in the air, underground is flat on ground
+ path: Vec3[];
+}
+
+export interface SceneData {
+ nodes: SceneNode[];
+ edges: SceneEdge[];
+}
+
+function rand(seed: number) {
+ const x = Math.sin(seed) * 10000;
+ return x - Math.floor(x);
+}
+
+export function generateSceneData(seed: number): SceneData {
+ let rSeed = seed;
+ const rng = () => rand(rSeed++);
+ const noise2D = createNoise2D(() => rng());
+
+ const nodes: SceneNode[] = [];
+ const edges: SceneEdge[] = [];
+ const substations: SceneNode[] = [];
+
+ // 1. Generation Areas (Wind Parks, Solar Farms, Cities)
+ // We want them spread across a large landscape (-400 to 400)
+ const mapSize = 800;
+
+ // Choose random central points for clusters
+ const windParks: Vec2[] = [];
+ const solarFarms: Vec2[] = [];
+ const cities: Vec2[] = [];
+
+ for (let i = 0; i < 4; i++) {
+ windParks.push([(rng() - 0.5) * mapSize, (rng() - 0.5) * mapSize]);
+ solarFarms.push([(rng() - 0.5) * mapSize, (rng() - 0.5) * mapSize]);
+ cities.push([(rng() - 0.5) * mapSize, (rng() - 0.5) * mapSize]);
+ }
+
+ // Push substations away from the centers slightly
+ const placeSubstation = (center: Vec2, clusterId: string) => {
+ const angle = rng() * Math.PI * 2;
+ const r = 20 + rng() * 10;
+ const sx = center[0] + Math.cos(angle) * r;
+ const sz = center[1] + Math.sin(angle) * r;
+ const sy = getTerrainHeight(noise2D, sx, sz);
+
+ const substation: SceneNode = {
+ id: `sub_${clusterId}`,
+ type: 'substation',
+ position: [sx, sy, sz],
+ rotation: [0, rng() * Math.PI, 0],
+ scale: [1, 1, 1],
+ clusterId
+ };
+ nodes.push(substation);
+ substations.push(substation);
+ return substation;
+ };
+
+ // Generate Wind Parks
+ windParks.forEach((center, idx) => {
+ const cid = `wind_park_${idx}`;
+ const sub = placeSubstation(center, cid);
+ const count = 10 + Math.floor(rng() * 10); // 10-20 turbines
+ const points = generateClusteredPoints(rng, center, count, 60, 15);
+
+ points.forEach((p, pIdx) => {
+ const y = getTerrainHeight(noise2D, p[0], p[1]);
+ const tid = `${cid}_t_${pIdx}`;
+ nodes.push({
+ id: tid,
+ type: 'wind',
+ position: [p[0], y, p[1]],
+ rotation: [0, rng() * Math.PI, 0],
+ scale: [1 + rng() * 0.2, 1 + rng() * 0.2, 1 + rng() * 0.2],
+ clusterId: cid
+ });
+
+ // Underground cable to substation
+ edges.push({
+ id: `edge_${tid}_${sub.id}`,
+ source: tid,
+ target: sub.id,
+ type: 'underground',
+ path: [[p[0], y, p[1]], sub.position]
+ });
+ });
+ });
+
+ // Generate Solar Farms
+ solarFarms.forEach((center, idx) => {
+ const cid = `solar_farm_${idx}`;
+ const sub = placeSubstation(center, cid);
+ const count = 30 + Math.floor(rng() * 20);
+ // Grid-like placement for solar
+ const points: Vec2[] = [];
+ for (let r = -3; r <= 3; r++) {
+ for (let c = -3; c <= 3; c++) {
+ if (rng() > 0.3) {
+ const sx = center[0] + r * 6 + (rng() - 0.5) * 2;
+ const sz = center[1] + c * 4 + (rng() - 0.5) * 2;
+ points.push([sx, sz]);
+ }
+ }
+ }
+
+ points.forEach((p, pIdx) => {
+ const y = getTerrainHeight(noise2D, p[0], p[1]);
+ const tid = `${cid}_s_${pIdx}`;
+ nodes.push({
+ id: tid,
+ type: 'solar',
+ position: [p[0], y, p[1]],
+ rotation: [0, 0, Math.PI * 0.1], // slight tilt up
+ scale: [1, 1, 1],
+ clusterId: cid
+ });
+
+ // Group cables not needed for every single solar panel, maybe just one underground per farm for visual
+ });
+ // Add one main underground cable from center of grid to substation
+ const yC = getTerrainHeight(noise2D, center[0], center[1]);
+ edges.push({
+ id: `edge_solar_main_${cid}_${sub.id}`,
+ source: cid,
+ target: sub.id,
+ type: 'underground',
+ path: [[center[0], yC, center[1]], sub.position]
+ });
+ });
+
+ // Generate Cities
+ cities.forEach((center, idx) => {
+ const cid = `city_${idx}`;
+ const sub = placeSubstation(center, cid);
+ const count = 20 + Math.floor(rng() * 20);
+ const points = generateClusteredPoints(rng, center, count, 50, 6);
+
+ points.forEach((p, pIdx) => {
+ const y = getTerrainHeight(noise2D, p[0], p[1]);
+ const tid = `${cid}_b_${pIdx}`;
+
+ const distToCenter = distance2D(p, center);
+ // taller buildings in center
+ const typeRand = rng();
+ let subType = 'small';
+ if (distToCenter < 20 && typeRand > 0.4) subType = 'tall';
+ else if (distToCenter < 35 && typeRand > 0.3) subType = 'medium';
+
+ const sY = subType === 'tall' ? 4 + rng() * 4 : subType === 'medium' ? 2 + rng() * 2 : 1 + rng();
+
+ nodes.push({
+ id: tid,
+ type: 'city_building',
+ position: [p[0], y + sY / 2, p[1]], // elevate by half height so it sits on ground
+ rotation: [0, rng() * Math.PI, 0],
+ scale: [1 + rng(), sY, 1 + rng()],
+ clusterId: cid,
+ subType
+ });
+ });
+ });
+
+ // Connect Substations via high-voltage lines
+ // A simple minimum spanning tree (MST) or nearest neighbor chain
+ const unvisited = [...substations];
+ const visited: SceneNode[] = [];
+ if (unvisited.length > 0) {
+ visited.push(unvisited.pop()!);
+ }
+
+ const towersIdMap = new Map();
+ let towerCount = 0;
+
+ while (unvisited.length > 0) {
+ let bestDist = Infinity;
+ let bestFrom: SceneNode | null = null;
+ let bestTo: SceneNode | null = null;
+ let bestIdx = -1;
+
+ for (const v of visited) {
+ for (let i = 0; i < unvisited.length; i++) {
+ const u = unvisited[i];
+ const d = distance3D(v.position, u.position);
+ if (d < bestDist) {
+ bestDist = d;
+ bestFrom = v;
+ bestTo = u;
+ bestIdx = i;
+ }
+ }
+ }
+
+ if (bestFrom && bestTo) {
+ visited.push(bestTo);
+ unvisited.splice(bestIdx, 1);
+
+ // Interpolate towers between bestFrom and bestTo
+ const segLen = 60;
+ const steps = Math.max(1, Math.floor(bestDist / segLen));
+ let prevPoint: SceneNode = bestFrom;
+
+ for (let s = 1; s <= steps; s++) {
+ const t = s / (steps + 1);
+ const tx = bestFrom.position[0] + (bestTo.position[0] - bestFrom.position[0]) * t;
+ const tz = bestFrom.position[2] + (bestTo.position[2] - bestFrom.position[2]) * t;
+ const ty = getTerrainHeight(noise2D, tx, tz);
+
+ const isLast = s === steps + 1; // wait, loop is to steps. Next is bestTo.
+
+ const newTower: SceneNode = {
+ id: `tower_${++towerCount}`,
+ type: 'tower',
+ position: [tx, ty, tz],
+ rotation: [0, Math.atan2(bestTo.position[0] - bestFrom.position[0], bestTo.position[2] - bestFrom.position[2]), 0],
+ scale: [1, 1, 1]
+ };
+
+ nodes.push(newTower);
+
+ // Cable from prevPoint to newTower
+ // Tower height offset
+ const h1 = prevPoint.type === 'tower' ? 12 : 3;
+ const h2 = newTower.type === 'tower' ? 12 : 3;
+
+ const p1: Vec3 = [prevPoint.position[0], prevPoint.position[1] + h1, prevPoint.position[2]];
+ const p2: Vec3 = [newTower.position[0], newTower.position[1] + h2, newTower.position[2]];
+
+ edges.push({
+ id: `edge_hv_${prevPoint.id}_${newTower.id}`,
+ source: prevPoint.id,
+ target: newTower.id,
+ type: 'transmission',
+ path: generateCatenaryCurve(p1, p2, 5, 8) // sag of 5, 8 segments
+ });
+
+ prevPoint = newTower;
+ }
+
+ // connect prevPoint to bestTo
+ const h1 = prevPoint.type === 'tower' ? 12 : 3;
+ const h2 = bestTo.type === 'tower' ? 12 : 3;
+ const p1: Vec3 = [prevPoint.position[0], prevPoint.position[1] + h1, prevPoint.position[2]];
+ const p2: Vec3 = [bestTo.position[0], bestTo.position[1] + h2, bestTo.position[2]];
+
+ edges.push({
+ id: `edge_hv_${prevPoint.id}_${bestTo.id}`,
+ source: prevPoint.id,
+ target: bestTo.id,
+ type: 'transmission',
+ path: generateCatenaryCurve(p1, p2, 5, 8)
+ });
+ }
+ }
+
+ // Generate Trees over empty areas
+ // 1000 trees using random noise clustering
+ for (let i = 0; i < 800; i++) {
+ const tx = (rng() - 0.5) * mapSize;
+ const tz = (rng() - 0.5) * mapSize;
+
+ // Quick check to avoid intersecting cities/substations (simple distance)
+ let tooClose = false;
+ for (const sub of substations) {
+ if (distance2D([tx, tz], [sub.position[0], sub.position[2]]) < 40) {
+ tooClose = true;
+ break;
+ }
+ }
+
+ if (!tooClose) {
+ const ty = getTerrainHeight(noise2D, tx, tz);
+ // Only place trees slightly lower on hills, not in water (if any) and clustered
+ const n = noise2D(tx * 0.005, tz * 0.005);
+ if (n > -0.2) {
+ nodes.push({
+ id: `tree_${i}`,
+ type: 'tree',
+ position: [tx, ty, tz],
+ rotation: [0, rng() * Math.PI, 0],
+ scale: [1 + rng() * 0.5, 1 + rng() * 0.5, 1 + rng() * 0.5]
+ });
+ }
+ }
+ }
+
+ return { nodes, edges };
+}
diff --git a/components/home/hero-webgl/HeroWebGLScene.tsx b/components/home/hero-webgl/HeroWebGLScene.tsx
new file mode 100644
index 00000000..f5147b9d
--- /dev/null
+++ b/components/home/hero-webgl/HeroWebGLScene.tsx
@@ -0,0 +1,54 @@
+import React, { useMemo, useRef } from 'react';
+import { Canvas, useFrame } from '@react-three/fiber';
+import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
+import { generateSceneData } from './Generator';
+import ObjectInstances from './ObjectInstances';
+import TransmissionLines from './TransmissionLines';
+import Landscape from './Landscape';
+import * as THREE from 'three';
+
+const WebGLContent: React.FC = () => {
+ // Generate the procedural data with a fixed seed so it's consistent
+ const sceneData = useMemo(() => generateSceneData(42), []);
+
+ const groupRef = useRef(null!);
+
+ // Very slow rotation for a cinematic feel
+ useFrame((state, delta) => {
+ if (groupRef.current) {
+ groupRef.current.rotation.y += delta * 0.05;
+ }
+ });
+
+ return (
+ <>
+
+
+
+ {/* Atmospheric Fog - blends the edges into the space background */}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default function HeroWebGLScene() {
+ return (
+
+
+ {/* Decorative overlaid gradient exactly like space */}
+
+
+ );
+}
diff --git a/components/home/hero-webgl/Landscape.tsx b/components/home/hero-webgl/Landscape.tsx
new file mode 100644
index 00000000..c8ce44ca
--- /dev/null
+++ b/components/home/hero-webgl/Landscape.tsx
@@ -0,0 +1,61 @@
+import React, { useMemo } from 'react';
+import * as THREE from 'three';
+import { createNoise2D } from 'simplex-noise';
+import { getTerrainHeight } from './math';
+
+export const Landscape: React.FC<{ seed: number }> = ({ seed }) => {
+ const geometry = useMemo(() => {
+ // Generate a very large plane with many segments
+ const size = 1200;
+ const segments = 128;
+ const geo = new THREE.PlaneGeometry(size, size, segments, segments);
+ geo.rotateX(-Math.PI / 2); // Lay flat on XZ plane
+
+ const pos = geo.attributes.position;
+ const vertex = new THREE.Vector3();
+
+ // Recreate same noise instance the Generator used
+ let rSeed = seed;
+ const rng = () => {
+ const x = Math.sin(rSeed++) * 10000;
+ return x - Math.floor(x);
+ };
+ const noise2D = createNoise2D(() => rng());
+
+ for (let i = 0; i < pos.count; i++) {
+ vertex.fromBufferAttribute(pos, i);
+
+ // Main height from the math utility so objects match exactly
+ const h = getTerrainHeight(noise2D, vertex.x, vertex.z);
+
+ // Curve down massively at edges to form a "planet" horizon
+ // If we are at dist R from center, we drop it down smoothly.
+ const dist = Math.sqrt(vertex.x * vertex.x + vertex.z * vertex.z);
+ const edgeRadius = 400;
+ let drop = 0;
+ if (dist > edgeRadius) {
+ // Drop quadratically beyond edgeRadius
+ const d = dist - edgeRadius;
+ drop = (d * d) * 0.05;
+ }
+
+ pos.setY(i, h - drop);
+ }
+
+ geo.computeVertexNormals();
+ return geo;
+ }, [seed]);
+
+ return (
+
+
+
+ );
+};
+
+export default Landscape;
diff --git a/components/home/hero-webgl/ObjectInstances.tsx b/components/home/hero-webgl/ObjectInstances.tsx
new file mode 100644
index 00000000..14eb0aea
--- /dev/null
+++ b/components/home/hero-webgl/ObjectInstances.tsx
@@ -0,0 +1,126 @@
+import React, { useMemo } from 'react';
+import { useFrame } from '@react-three/fiber';
+import { SceneData, SceneNode } from './Generator';
+import * as THREE from 'three';
+
+const ObjectInstances: React.FC<{ data: SceneData }> = ({ data }) => {
+ // We'll separate nodes by type to feed into distinct InstancedMeshes
+ const windNodes = useMemo(() => data.nodes.filter(n => n.type === 'wind'), [data]);
+ const solarNodes = useMemo(() => data.nodes.filter(n => n.type === 'solar'), [data]);
+ const cityNodes = useMemo(() => data.nodes.filter(n => n.type === 'city_building'), [data]);
+ const treeNodes = useMemo(() => data.nodes.filter(n => n.type === 'tree'), [data]);
+ const towerNodes = useMemo(() => data.nodes.filter(n => n.type === 'tower'), [data]);
+ const subNodes = useMemo(() => data.nodes.filter(n => n.type === 'substation'), [data]);
+
+ const setMatrixAt = (mesh: THREE.InstancedMesh, i: number, node: SceneNode) => {
+ const obj = new THREE.Object3D();
+ obj.position.set(node.position[0], node.position[1], node.position[2]);
+ obj.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2]);
+ obj.scale.set(node.scale[0], node.scale[1], node.scale[2]);
+ obj.updateMatrix();
+ mesh.setMatrixAt(i, obj.matrix);
+ };
+
+ // Precompute instance matrices
+ const windMatrices = useMemo(() => {
+ const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial(), windNodes.length);
+ windNodes.forEach((node, i) => setMatrixAt(mesh, i, node));
+ mesh.instanceMatrix.needsUpdate = true;
+ return mesh.instanceMatrix;
+ }, [windNodes]);
+
+ const solarMatrices = useMemo(() => {
+ const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial(), solarNodes.length);
+ solarNodes.forEach((node, i) => setMatrixAt(mesh, i, node));
+ mesh.instanceMatrix.needsUpdate = true;
+ return mesh.instanceMatrix;
+ }, [solarNodes]);
+
+ const treeMatrices = useMemo(() => {
+ const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial(), treeNodes.length);
+ treeNodes.forEach((node, i) => setMatrixAt(mesh, i, node));
+ mesh.instanceMatrix.needsUpdate = true;
+ return mesh.instanceMatrix;
+ }, [treeNodes]);
+
+ const towerMatrices = useMemo(() => {
+ const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial(), towerNodes.length);
+ towerNodes.forEach((node, i) => setMatrixAt(mesh, i, node));
+ mesh.instanceMatrix.needsUpdate = true;
+ return mesh.instanceMatrix;
+ }, [towerNodes]);
+
+ const subMatrices = useMemo(() => {
+ const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial(), subNodes.length);
+ subNodes.forEach((node, i) => setMatrixAt(mesh, i, node));
+ mesh.instanceMatrix.needsUpdate = true;
+ return mesh.instanceMatrix;
+ }, [subNodes]);
+
+ const cityMatrices = useMemo(() => {
+ const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial(), cityNodes.length);
+ cityNodes.forEach((node, i) => setMatrixAt(mesh, i, node));
+ mesh.instanceMatrix.needsUpdate = true;
+ return mesh.instanceMatrix;
+ }, [cityNodes]);
+
+ return (
+
+ {/* Trees */}
+ {treeNodes.length > 0 && (
+
+
+
+
+
+ )}
+
+ {/* Wind Turbines Towers */}
+ {windNodes.length > 0 && (
+
+
+
+
+
+ )}
+
+ {/* Solar Panels */}
+ {solarNodes.length > 0 && (
+
+
+
+
+
+ )}
+
+ {/* City Buildings */}
+ {cityNodes.length > 0 && (
+
+
+
+
+
+ )}
+
+ {/* Substations */}
+ {subNodes.length > 0 && (
+
+
+
+
+
+ )}
+
+ {/* Transition Towers (Grid) */}
+ {towerNodes.length > 0 && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default ObjectInstances;
diff --git a/components/home/hero-webgl/TransmissionLines.tsx b/components/home/hero-webgl/TransmissionLines.tsx
new file mode 100644
index 00000000..468d9570
--- /dev/null
+++ b/components/home/hero-webgl/TransmissionLines.tsx
@@ -0,0 +1,59 @@
+import React, { useMemo, useRef } from 'react';
+import { useFrame } from '@react-three/fiber';
+import { SceneData, SceneEdge } from './Generator';
+import * as THREE from 'three';
+import { Line } from '@react-three/drei';
+
+// A single CatmullRomCurve3 cable with particles moving along it
+const Cable: React.FC<{ edge: SceneEdge }> = ({ edge }) => {
+ const points = useMemo(() => edge.path.map(p => new THREE.Vector3(p[0], p[1], p[2])), [edge.path]);
+ const curve = useMemo(() => new THREE.CatmullRomCurve3(points), [points]);
+
+ // We extract the line points for rendering the static line
+ const linePoints = useMemo(() => curve.getPoints(20), [curve]);
+ const linePositions = useMemo(() => linePoints.flatMap(p => [p.x, p.y, p.z]), [linePoints]);
+
+ const particleRef = useRef(null!);
+ const timeRef = useRef(Math.random()); // Random offset for particles
+
+ useFrame((state, delta) => {
+ if (!particleRef.current) return;
+
+ // Move particle along the curve. Speed based on edge type.
+ const speed = edge.type === 'transmission' ? 0.3 : 0.15;
+ timeRef.current = (timeRef.current + delta * speed) % 1;
+
+ const pos = curve.getPointAt(timeRef.current);
+ particleRef.current.position.copy(pos);
+ });
+
+ return (
+
+ {/* The actual cable. Underground cables can just be transparent or slightly visible */}
+ {edge.type === 'transmission' && (
+
+ )}
+ {/* The glowing particle */}
+
+
+
+
+
+ );
+};
+
+const TransmissionLines: React.FC<{ data: SceneData }> = ({ data }) => {
+ return (
+
+ {data.edges.map(edge => (
+
+ ))}
+
+ );
+};
+
+export default TransmissionLines;
diff --git a/components/home/hero-webgl/math.ts b/components/home/hero-webgl/math.ts
new file mode 100644
index 00000000..d9803cb1
--- /dev/null
+++ b/components/home/hero-webgl/math.ts
@@ -0,0 +1,98 @@
+import { createNoise2D } from 'simplex-noise';
+
+// Seeded random number generator
+export function LCG(seed: number) {
+ return function () {
+ seed = Math.imul(48271, seed) | 0 % 2147483647;
+ return (seed & 2147483647) / 2147483648;
+ };
+}
+
+export type Vec2 = [number, number];
+export type Vec3 = [number, number, number];
+
+export function distance2D(a: Vec2, b: Vec2) {
+ const dx = a[0] - b[0];
+ const dy = a[1] - b[1];
+ return Math.sqrt(dx * dx + dy * dy);
+}
+
+export function distance3D(a: Vec3, b: Vec3) {
+ const dx = a[0] - b[0];
+ const dy = a[1] - b[1];
+ const dz = a[2] - b[2];
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+}
+
+// Generate points for a hanging cable between two points (catenary/sag)
+export function generateCatenaryCurve(
+ p1: Vec3,
+ p2: Vec3,
+ sag: number,
+ segments: number = 10
+): Vec3[] {
+ const points: Vec3[] = [];
+ for (let i = 0; i <= segments; i++) {
+ const t = i / segments;
+ const x = p1[0] + (p2[0] - p1[0]) * t;
+ const z = p1[2] + (p2[2] - p1[2]) * t;
+
+ // Linear interpolation of height
+ const baseHeight = p1[1] + (p2[1] - p1[1]) * t;
+
+ // Add parabolic sag
+ // t ranges from 0 to 1. Parabola that is 0 at ends and 1 in middle is 4 * t * (1 - t)
+ const sagOffset = 4 * t * (1 - t) * sag;
+
+ const y = baseHeight - sagOffset;
+ points.push([x, y, z]);
+ }
+ return points;
+}
+
+// Simple terrain height function combining multiple frequencies
+export function getTerrainHeight(noise2D: ReturnType, x: number, z: number): number {
+ // We want a gigantic, hilly planet. We'll use low frequency noise.
+ const n1 = noise2D(x * 0.002, z * 0.002) * 40; // Main hills
+ const n2 = noise2D(x * 0.01, z * 0.01) * 10; // Smaller details
+ return n1 + n2;
+}
+
+// Helper: Place points within a radius using rejection sampling to avoid overlap
+export function generateClusteredPoints(
+ rng: () => number,
+ center: Vec2,
+ count: number,
+ radius: number,
+ minDist: number
+): Vec2[] {
+ const points: Vec2[] = [];
+ let attempts = 0;
+ const maxAttempts = count * 50;
+
+ while (points.length < count && attempts < maxAttempts) {
+ attempts++;
+ const angle = rng() * Math.PI * 2;
+ // Square root for uniform distribution in a circle
+ const r = Math.sqrt(rng()) * radius;
+
+ const p: Vec2 = [
+ center[0] + Math.cos(angle) * r,
+ center[1] + Math.sin(angle) * r
+ ];
+
+ let valid = true;
+ for (const other of points) {
+ if (distance2D(p, other) < minDist) {
+ valid = false;
+ break;
+ }
+ }
+
+ if (valid) {
+ points.push(p);
+ }
+ }
+
+ return points;
+}
diff --git a/package.json b/package.json
index f8337656..e75a3ed0 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"resend": "^3.5.0",
"schema-dts": "^1.1.5",
"sharp": "^0.34.5",
+ "simplex-noise": "^4.0.3",
"svg-to-pdfkit": "^0.1.8",
"tailwind-merge": "^3.4.0",
"three": "^0.183.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b65c5132..568b0148 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -138,6 +138,9 @@ importers:
sharp:
specifier: ^0.34.5
version: 0.34.5
+ simplex-noise:
+ specifier: ^4.0.3
+ version: 4.0.3
svg-to-pdfkit:
specifier: ^0.1.8
version: 0.1.8
@@ -7552,6 +7555,9 @@ packages:
simple-wcswidth@1.1.2:
resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==}
+ simplex-noise@4.0.3:
+ resolution: {integrity: sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==}
+
sirv@2.0.4:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
engines: {node: '>= 10'}
@@ -16826,6 +16832,8 @@ snapshots:
simple-wcswidth@1.1.2: {}
+ simplex-noise@4.0.3: {}
+
sirv@2.0.4:
dependencies:
'@polka/url': 1.0.0-next.29