diff --git a/components/home/HeroIllustration.tsx b/components/home/HeroIllustration.tsx index e4b687ee..1b065bcb 100644 --- a/components/home/HeroIllustration.tsx +++ b/components/home/HeroIllustration.tsx @@ -1,8 +1,410 @@ 'use client'; -import React from 'react'; -import HeroWebGLScene from './hero-webgl/HeroWebGLScene'; +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 } }, +]; export default function HeroIllustration() { - return ; + 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 ( +
+ +
+
+ ); } diff --git a/components/home/hero-webgl/Generator.ts b/components/home/hero-webgl/Generator.ts deleted file mode 100644 index ac2c8754..00000000 --- a/components/home/hero-webgl/Generator.ts +++ /dev/null @@ -1,307 +0,0 @@ -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 deleted file mode 100644 index f5147b9d..00000000 --- a/components/home/hero-webgl/HeroWebGLScene.tsx +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index c8ce44ca..00000000 --- a/components/home/hero-webgl/Landscape.tsx +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 14eb0aea..00000000 --- a/components/home/hero-webgl/ObjectInstances.tsx +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index 468d9570..00000000 --- a/components/home/hero-webgl/TransmissionLines.tsx +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index d9803cb1..00000000 --- a/components/home/hero-webgl/math.ts +++ /dev/null @@ -1,98 +0,0 @@ -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 e75a3ed0..f8337656 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "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 568b0148..b65c5132 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,9 +138,6 @@ 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 @@ -7555,9 +7552,6 @@ 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'} @@ -16832,8 +16826,6 @@ snapshots: simple-wcswidth@1.1.2: {} - simplex-noise@4.0.3: {} - sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29