data endpoint for entity 90008 (aka. a website)

set sharp concurrency to 1 to reduce memory fragmentation

ptr.pet e0d516c0 a920adb5

verified
Waiting for spindle ...
+590 -546
+590 -546
eunomia/src/lib/constellation.ts
··· 15 15 const GRAPH_URL = 'https://eightyeightthirty.one/graph.json'; 16 16 17 17 type GraphData = { 18 - linksTo: Record<string, string[]>; 19 - } 18 + linksTo: Record<string, string[]>; 19 + }; 20 20 21 21 type Star = { 22 - domain: string; 23 - x: number; 24 - y: number; 25 - z: number; 26 - connections: string[]; 27 - visualConnections: string[]; 28 - } 22 + domain: string; 23 + x: number; 24 + y: number; 25 + z: number; 26 + connections: string[]; 27 + visualConnections: string[]; 28 + }; 29 29 30 - export type Nebula = { x: number, y: number, z: number, density: number }; 31 - export type Dust = { x: number, y: number, z: number, alpha: number, sizeFactor: number, color: string }; 30 + export type Nebula = { x: number; y: number; z: number; density: number }; 31 + export type Dust = { 32 + x: number; 33 + y: number; 34 + z: number; 35 + alpha: number; 36 + sizeFactor: number; 37 + color: string; 38 + }; 32 39 33 40 export type ConstellationData = { 34 - stars: Star[]; 35 - nebulae: Nebula[]; 36 - dust: Dust[]; 41 + stars: Star[]; 42 + nebulae: Nebula[]; 43 + dust: Dust[]; 37 44 }; 38 45 39 46 // Deterministic implementation with SeededRNG 40 47 class SeededRNG { 41 - private seed: number; 42 - constructor(seed: number) { 43 - this.seed = seed; 44 - } 48 + private seed: number; 49 + constructor(seed: number) { 50 + this.seed = seed; 51 + } 45 52 46 - next(): number { 47 - this.seed = (this.seed * 1664525 + 1013904223) % 4294967296; 48 - return this.seed / 4294967296; 49 - } 53 + next(): number { 54 + this.seed = (this.seed * 1664525 + 1013904223) % 4294967296; 55 + return this.seed / 4294967296; 56 + } 50 57 51 - range(min: number, max: number): number { 52 - return min + this.next() * (max - min); 53 - } 58 + range(min: number, max: number): number { 59 + return min + this.next() * (max - min); 60 + } 54 61 } 55 62 56 - export const generateConstellationData = (data: GraphData, seed: number = 123456): ConstellationData => { 57 - const rng = new SeededRNG(seed); 58 - 59 - // Configuration 60 - const MAX_STARS = 2000; 61 - const CLUSTER_SIZE_MAX = 8; 62 - const MIN_DIST = 140; 63 - const STEP_SIZE = 150; 64 - const ATTEMPTS = 32; 63 + export const generateConstellationData = ( 64 + data: GraphData, 65 + seed: number = 123456 66 + ): ConstellationData => { 67 + const rng = new SeededRNG(seed); 65 68 66 - // Limits 67 - const ROOT_DOMAIN_LIMIT_RATIO = 0.025; // 2.5% of MAX_STARS 68 - const TOTAL_ROOT_DOMAIN_LIMIT_RATIO = 0.2; // 20% of MAX_STARS 69 - const MAX_PER_ROOT = Math.floor(MAX_STARS * ROOT_DOMAIN_LIMIT_RATIO); 70 - const MAX_TOTAL_ROOT = Math.floor(MAX_STARS * TOTAL_ROOT_DOMAIN_LIMIT_RATIO); 71 - const SPECIAL_ROOTS = new Set(['neocities.org', 'wordpress.com', 'blogspot.com', 'blogfree.net', 'forumfree.it', 'forumcommunity.net', 'proboards.com', 'boards.net', 'jcink.net', 'forumactif.net', 'forumactif.org', 'forumactif.com', 'freeforums.net']); 69 + // Configuration 70 + const MAX_STARS = 2000; 71 + const CLUSTER_SIZE_MAX = 8; 72 + const MIN_DIST = 140; 73 + const STEP_SIZE = 150; 74 + const ATTEMPTS = 32; 72 75 73 - const getRootDomain = (domain: string): string => { 74 - const parts = domain.split('.'); 75 - if (parts.length > 2) { 76 - const root = parts.slice(-2).join('.'); 77 - if (SPECIAL_ROOTS.has(root)) return root; 78 - } 79 - return domain; // Default to full domain if not special 80 - }; 76 + // Limits 77 + const ROOT_DOMAIN_LIMIT_RATIO = 0.025; // 2.5% of MAX_STARS 78 + const TOTAL_ROOT_DOMAIN_LIMIT_RATIO = 0.2; // 20% of MAX_STARS 79 + const MAX_PER_ROOT = Math.floor(MAX_STARS * ROOT_DOMAIN_LIMIT_RATIO); 80 + const MAX_TOTAL_ROOT = Math.floor(MAX_STARS * TOTAL_ROOT_DOMAIN_LIMIT_RATIO); 81 + const SPECIAL_ROOTS = new Set([ 82 + 'neocities.org', 83 + 'wordpress.com', 84 + 'blogspot.com', 85 + 'blogfree.net', 86 + 'forumfree.it', 87 + 'forumcommunity.net', 88 + 'proboards.com', 89 + 'boards.net', 90 + 'jcink.net', 91 + 'forumactif.net', 92 + 'forumactif.org', 93 + 'forumactif.com', 94 + 'freeforums.net' 95 + ]); 81 96 82 - const rootDomainCounts = new Map<string, number>(); 97 + const getRootDomain = (domain: string): string => { 98 + const parts = domain.split('.'); 99 + if (parts.length > 2) { 100 + const root = parts.slice(-2).join('.'); 101 + if (SPECIAL_ROOTS.has(root)) return root; 102 + } 103 + return domain; // Default to full domain if not special 104 + }; 83 105 84 - // 1. Filter and Collect Stars 85 - const allDomains = Object.keys(data.linksTo); 86 - const validDomains = new Set<string>(allDomains); 106 + const rootDomainCounts = new Map<string, number>(); 87 107 88 - // 2. Greedy Clustering 89 - const stars: Star[] = []; 90 - const visited = new Set<string>(); 91 - const clusters: { center: { x: number, y: number, z: number }, stars: Star[] }[] = []; 108 + // 1. Filter and Collect Stars 109 + const allDomains = Object.keys(data.linksTo); 110 + const validDomains = new Set<string>(allDomains); 92 111 93 - // Shuffle domains deterministically 94 - for (let i = allDomains.length - 1; i > 0; i--) { 95 - const j = Math.floor(rng.next() * (i + 1)); 96 - [allDomains[i], allDomains[j]] = [allDomains[j], allDomains[i]]; 97 - } 112 + // 2. Greedy Clustering 113 + const stars: Star[] = []; 114 + const visited = new Set<string>(); 115 + const clusters: { center: { x: number; y: number; z: number }; stars: Star[] }[] = []; 98 116 99 - for (const domain of allDomains) { 100 - if (visited.has(domain)) continue; 101 - if (stars.length >= MAX_STARS) break; 117 + // Shuffle domains deterministically 118 + for (let i = allDomains.length - 1; i > 0; i--) { 119 + const j = Math.floor(rng.next() * (i + 1)); 120 + [allDomains[i], allDomains[j]] = [allDomains[j], allDomains[i]]; 121 + } 102 122 103 - const root = getRootDomain(domain); 104 - const currentCount = rootDomainCounts.get(root) || 0; 123 + for (const domain of allDomains) { 124 + if (visited.has(domain)) continue; 125 + if (stars.length >= MAX_STARS) break; 105 126 106 - // Only limit if it is one of the special roots 107 - if (SPECIAL_ROOTS.has(root) && currentCount >= MAX_PER_ROOT) continue; 127 + const root = getRootDomain(domain); 128 + const currentCount = rootDomainCounts.get(root) || 0; 108 129 109 - // or if the total number of special roots exceeds the limit 110 - const totalSpecialCount = rootDomainCounts.entries().filter(([root]) => SPECIAL_ROOTS.has(root)).reduce((a, [, b]) => a + b, 0); 111 - if (totalSpecialCount >= MAX_TOTAL_ROOT) continue; 130 + // Only limit if it is one of the special roots 131 + if (SPECIAL_ROOTS.has(root) && currentCount >= MAX_PER_ROOT) continue; 112 132 113 - // Start a new cluster 114 - const clusterStars: Star[] = []; 115 - const stack: string[] = [domain]; 133 + // or if the total number of special roots exceeds the limit 134 + const totalSpecialCount = rootDomainCounts 135 + .entries() 136 + .filter(([root]) => SPECIAL_ROOTS.has(root)) 137 + .reduce((a, [, b]) => a + b, 0); 138 + if (totalSpecialCount >= MAX_TOTAL_ROOT) continue; 116 139 117 - // We do NOT add to visited yet, we do it when we actually push to clusterStars 140 + // Start a new cluster 141 + const clusterStars: Star[] = []; 142 + const stack: string[] = [domain]; 118 143 119 - const skeletonEdges: [string, string][] = []; 120 - const parents = new Map<string, string>(); 144 + // We do NOT add to visited yet, we do it when we actually push to clusterStars 121 145 122 - // Fill cluster (greedy DFS) 123 - while (stack.length > 0 && clusterStars.length < CLUSTER_SIZE_MAX) { 124 - const current = stack.pop()!; 146 + const skeletonEdges: [string, string][] = []; 147 + const parents = new Map<string, string>(); 125 148 126 - if (visited.has(current)) continue; 149 + // Fill cluster (greedy DFS) 150 + while (stack.length > 0 && clusterStars.length < CLUSTER_SIZE_MAX) { 151 + const current = stack.pop()!; 127 152 128 - const currentRoot = getRootDomain(current); 129 - const count = rootDomainCounts.get(currentRoot) || 0; 130 - if (SPECIAL_ROOTS.has(currentRoot) && count >= MAX_PER_ROOT) continue; 153 + if (visited.has(current)) continue; 131 154 132 - visited.add(current); 133 - rootDomainCounts.set(currentRoot, count + 1); 155 + const currentRoot = getRootDomain(current); 156 + const count = rootDomainCounts.get(currentRoot) || 0; 157 + if (SPECIAL_ROOTS.has(currentRoot) && count >= MAX_PER_ROOT) continue; 134 158 135 - const links = data.linksTo[current] || []; 159 + visited.add(current); 160 + rootDomainCounts.set(currentRoot, count + 1); 136 161 137 - clusterStars.push({ 138 - domain: current, 139 - x: 0, y: 0, z: 0, 140 - connections: links, 141 - visualConnections: [] 142 - }); 162 + const links = data.linksTo[current] || []; 143 163 144 - const parent = parents.get(current); 145 - if (parent) skeletonEdges.push([parent, current]); 164 + clusterStars.push({ 165 + domain: current, 166 + x: 0, 167 + y: 0, 168 + z: 0, 169 + connections: links, 170 + visualConnections: [] 171 + }); 146 172 147 - const neighbors: string[] = []; 148 - for (const link of links) { 149 - if (!visited.has(link) && validDomains.has(link)) { 150 - neighbors.push(link); 151 - // Do not mark visited here, wait until we pop 152 - parents.set(link, current); 153 - } 154 - } 173 + const parent = parents.get(current); 174 + if (parent) skeletonEdges.push([parent, current]); 155 175 156 - // Randomize neighbors for DFS 157 - for (let i = neighbors.length - 1; i > 0; i--) { 158 - const j = Math.floor(rng.next() * (i + 1)); 159 - [neighbors[i], neighbors[j]] = [neighbors[j], neighbors[i]]; 160 - } 161 - stack.push(...neighbors); 162 - } 176 + const neighbors: string[] = []; 177 + for (const link of links) { 178 + if (!visited.has(link) && validDomains.has(link)) { 179 + neighbors.push(link); 180 + // Do not mark visited here, wait until we pop 181 + parents.set(link, current); 182 + } 183 + } 163 184 164 - if (clusterStars.length > 0) { 165 - clusters.push({ center: { x: 0, y: 0, z: 0 }, stars: clusterStars }); 185 + // Randomize neighbors for DFS 186 + for (let i = neighbors.length - 1; i > 0; i--) { 187 + const j = Math.floor(rng.next() * (i + 1)); 188 + [neighbors[i], neighbors[j]] = [neighbors[j], neighbors[i]]; 189 + } 190 + stack.push(...neighbors); 191 + } 166 192 167 - // Map stars for quick lookup 168 - const clusterMap = new Map<string, Star>(); 169 - clusterStars.forEach(s => clusterMap.set(s.domain, s)); 193 + if (clusterStars.length > 0) { 194 + clusters.push({ center: { x: 0, y: 0, z: 0 }, stars: clusterStars }); 170 195 171 - // Build dual-linked visual connections 172 - skeletonEdges.forEach(([src, dst]) => { 173 - const s1 = clusterMap.get(src); 174 - const s2 = clusterMap.get(dst); 175 - if (s1 && s2) { 176 - s1.visualConnections.push(dst); 177 - s2.visualConnections.push(src); 178 - } 179 - }); 196 + // Map stars for quick lookup 197 + const clusterMap = new Map<string, Star>(); 198 + clusterStars.forEach((s) => clusterMap.set(s.domain, s)); 180 199 181 - stars.push(...clusterStars); 182 - } 183 - } 200 + // Build dual-linked visual connections 201 + skeletonEdges.forEach(([src, dst]) => { 202 + const s1 = clusterMap.get(src); 203 + const s2 = clusterMap.get(dst); 204 + if (s1 && s2) { 205 + s1.visualConnections.push(dst); 206 + s2.visualConnections.push(src); 207 + } 208 + }); 184 209 185 - // 3. Layout Clusters on Fibonacci Sphere 186 - const phi = Math.PI * (3 - Math.sqrt(5)); 187 - const sphereRadius = 1800; 210 + stars.push(...clusterStars); 211 + } 212 + } 188 213 189 - for (let i = 0; i < clusters.length; i++) { 190 - const y = 1 - (i / (clusters.length - 1)) * 2; 191 - const radiusAtY = Math.sqrt(1 - y * y); 192 - const theta = phi * i; 214 + // 3. Layout Clusters on Fibonacci Sphere 215 + const phi = Math.PI * (3 - Math.sqrt(5)); 216 + const sphereRadius = 1800; 193 217 194 - clusters[i].center = { 195 - x: Math.cos(theta) * radiusAtY * sphereRadius, 196 - y: y * sphereRadius, 197 - z: Math.sin(theta) * radiusAtY * sphereRadius 198 - }; 199 - } 218 + for (let i = 0; i < clusters.length; i++) { 219 + const y = 1 - (i / (clusters.length - 1)) * 2; 220 + const radiusAtY = Math.sqrt(1 - y * y); 221 + const theta = phi * i; 200 222 201 - // 4. Layout Stars (Directional Bias with Global Collision) 202 - const placedStars: Star[] = []; 223 + clusters[i].center = { 224 + x: Math.cos(theta) * radiusAtY * sphereRadius, 225 + y: y * sphereRadius, 226 + z: Math.sin(theta) * radiusAtY * sphereRadius 227 + }; 228 + } 203 229 204 - // Helper: Rotate vector v around random axis by angle 205 - const rotateVector = (v: { dx: number, dy: number, dz: number }, angle: number) => { 206 - // Random axis perpendicular to v 207 - const rx = rng.next() - 0.5; 208 - const ry = rng.next() - 0.5; 209 - const rz = rng.next() - 0.5; 230 + // 4. Layout Stars (Directional Bias with Global Collision) 231 + const placedStars: Star[] = []; 210 232 211 - // Cross product v x r 212 - let cpx = v.dy * rz - v.dz * ry; 213 - let cpy = v.dz * rx - v.dx * rz; 214 - let cpz = v.dx * ry - v.dy * rx; 233 + // Helper: Rotate vector v around random axis by angle 234 + const rotateVector = (v: { dx: number; dy: number; dz: number }, angle: number) => { 235 + // Random axis perpendicular to v 236 + const rx = rng.next() - 0.5; 237 + const ry = rng.next() - 0.5; 238 + const rz = rng.next() - 0.5; 215 239 216 - let cpLen = Math.sqrt(cpx * cpx + cpy * cpy + cpz * cpz); 217 - if (cpLen < 0.001) { cpx = 1; cpy = 0; cpz = 0; cpLen = 1; } 240 + // Cross product v x r 241 + let cpx = v.dy * rz - v.dz * ry; 242 + let cpy = v.dz * rx - v.dx * rz; 243 + let cpz = v.dx * ry - v.dy * rx; 218 244 219 - // Axis k (normalized) 220 - const kx = cpx / cpLen; 221 - const ky = cpy / cpLen; 222 - const kz = cpz / cpLen; 245 + let cpLen = Math.sqrt(cpx * cpx + cpy * cpy + cpz * cpz); 246 + if (cpLen < 0.001) { 247 + cpx = 1; 248 + cpy = 0; 249 + cpz = 0; 250 + cpLen = 1; 251 + } 223 252 224 - // Rodrigues rotation: v_rot = v * cos(a) + (k x v) * sin(a) 225 - const cos = Math.cos(angle); 226 - const sin = Math.sin(angle); 253 + // Axis k (normalized) 254 + const kx = cpx / cpLen; 255 + const ky = cpy / cpLen; 256 + const kz = cpz / cpLen; 227 257 228 - // k x v 229 - const kxv_x = ky * v.dz - kz * v.dy; 230 - const kxv_y = kz * v.dx - kx * v.dz; 231 - const kxv_z = kx * v.dy - ky * v.dx; 258 + // Rodrigues rotation: v_rot = v * cos(a) + (k x v) * sin(a) 259 + const cos = Math.cos(angle); 260 + const sin = Math.sin(angle); 232 261 233 - const newDx = v.dx * cos + kxv_x * sin; 234 - const newDy = v.dy * cos + kxv_y * sin; 235 - const newDz = v.dz * cos + kxv_z * sin; 262 + // k x v 263 + const kxv_x = ky * v.dz - kz * v.dy; 264 + const kxv_y = kz * v.dx - kx * v.dz; 265 + const kxv_z = kx * v.dy - ky * v.dx; 236 266 237 - const len = Math.sqrt(newDx * newDx + newDy * newDy + newDz * newDz); 238 - return { dx: newDx / len, dy: newDy / len, dz: newDz / len }; 239 - }; 267 + const newDx = v.dx * cos + kxv_x * sin; 268 + const newDy = v.dy * cos + kxv_y * sin; 269 + const newDz = v.dz * cos + kxv_z * sin; 240 270 241 - // Helper: Check collision 242 - const checkCollision = (x: number, y: number, z: number) => { 243 - const minDistSq = MIN_DIST * MIN_DIST; 244 - for (const s of placedStars) { 245 - const d2 = (s.x - x) ** 2 + (s.y - y) ** 2 + (s.z - z) ** 2; 246 - if (d2 < minDistSq) return true; 247 - } 248 - return false; 249 - }; 271 + const len = Math.sqrt(newDx * newDx + newDy * newDy + newDz * newDz); 272 + return { dx: newDx / len, dy: newDy / len, dz: newDz / len }; 273 + }; 250 274 251 - for (const cluster of clusters) { 252 - if (cluster.stars.length === 0) continue; 275 + // Helper: Check collision 276 + const checkCollision = (x: number, y: number, z: number) => { 277 + const minDistSq = MIN_DIST * MIN_DIST; 278 + for (const s of placedStars) { 279 + const d2 = (s.x - x) ** 2 + (s.y - y) ** 2 + (s.z - z) ** 2; 280 + if (d2 < minDistSq) return true; 281 + } 282 + return false; 283 + }; 253 284 254 - // Root star 255 - const first = cluster.stars[0]; 256 - first.x = cluster.center.x; 257 - first.y = cluster.center.y; 258 - first.z = cluster.center.z; 285 + for (const cluster of clusters) { 286 + if (cluster.stars.length === 0) continue; 259 287 260 - placedStars.push(first); 261 - const placedInCluster = new Set<string>([first.domain]); 288 + // Root star 289 + const first = cluster.stars[0]; 290 + first.x = cluster.center.x; 291 + first.y = cluster.center.y; 292 + first.z = cluster.center.z; 262 293 263 - // Initial random direction 264 - const u = rng.next(); 265 - const v = rng.next(); 266 - const theta = 2 * Math.PI * u; 267 - const phi = Math.acos(2 * v - 1); 294 + placedStars.push(first); 295 + const placedInCluster = new Set<string>([first.domain]); 268 296 269 - const directions = new Map<string, { dx: number, dy: number, dz: number }>(); 270 - directions.set(first.domain, { 271 - dx: Math.sin(phi) * Math.cos(theta), 272 - dy: Math.sin(phi) * Math.sin(theta), 273 - dz: Math.cos(phi) 274 - }); 297 + // Initial random direction 298 + const u = rng.next(); 299 + const v = rng.next(); 300 + const theta = 2 * Math.PI * u; 301 + const phi = Math.acos(2 * v - 1); 275 302 276 - const layoutQueue = [first]; 303 + const directions = new Map<string, { dx: number; dy: number; dz: number }>(); 304 + directions.set(first.domain, { 305 + dx: Math.sin(phi) * Math.cos(theta), 306 + dy: Math.sin(phi) * Math.sin(theta), 307 + dz: Math.cos(phi) 308 + }); 277 309 278 - while (layoutQueue.length > 0) { 279 - const current = layoutQueue.shift()!; 280 - const prevDir = directions.get(current.domain)!; 310 + const layoutQueue = [first]; 281 311 282 - const unplacedNeighbors = current.visualConnections 283 - .map(id => cluster.stars.find(s => s.domain === id)) 284 - .filter(s => s && !placedInCluster.has(s.domain)) as Star[]; 312 + while (layoutQueue.length > 0) { 313 + const current = layoutQueue.shift()!; 314 + const prevDir = directions.get(current.domain)!; 285 315 286 - for (const target of unplacedNeighbors) { 287 - let bestPos = { x: 0, y: 0, z: 0 }; 288 - let bestDir = prevDir; 289 - let placed = false; 316 + const unplacedNeighbors = current.visualConnections 317 + .map((id) => cluster.stars.find((s) => s.domain === id)) 318 + .filter((s) => s && !placedInCluster.has(s.domain)) as Star[]; 290 319 291 - // Try multiple directions 292 - for (let i = 0; i < ATTEMPTS; i++) { 293 - const angle = (30 + rng.next() * 60) * (Math.PI / 180); 294 - const newDir = rotateVector(prevDir, angle); 320 + for (const target of unplacedNeighbors) { 321 + let bestPos = { x: 0, y: 0, z: 0 }; 322 + let bestDir = prevDir; 323 + let placed = false; 295 324 296 - const cx = current.x + newDir.dx * STEP_SIZE; 297 - const cy = current.y + newDir.dy * STEP_SIZE; 298 - const cz = current.z + newDir.dz * STEP_SIZE; 325 + // Try multiple directions 326 + for (let i = 0; i < ATTEMPTS; i++) { 327 + const angle = (30 + rng.next() * 60) * (Math.PI / 180); 328 + const newDir = rotateVector(prevDir, angle); 299 329 300 - if (!checkCollision(cx, cy, cz)) { 301 - bestPos = { x: cx, y: cy, z: cz }; 302 - bestDir = newDir; 303 - placed = true; 304 - break; 305 - } 306 - } 330 + const cx = current.x + newDir.dx * STEP_SIZE; 331 + const cy = current.y + newDir.dy * STEP_SIZE; 332 + const cz = current.z + newDir.dz * STEP_SIZE; 307 333 308 - // Fallback: Force straight line 309 - if (!placed) { 310 - bestPos = { 311 - x: current.x + prevDir.dx * STEP_SIZE, 312 - y: current.y + prevDir.dy * STEP_SIZE, 313 - z: current.z + prevDir.dz * STEP_SIZE 314 - }; 315 - bestDir = prevDir; 316 - } 334 + if (!checkCollision(cx, cy, cz)) { 335 + bestPos = { x: cx, y: cy, z: cz }; 336 + bestDir = newDir; 337 + placed = true; 338 + break; 339 + } 340 + } 317 341 318 - target.x = bestPos.x; 319 - target.y = bestPos.y; 320 - target.z = bestPos.z; 342 + // Fallback: Force straight line 343 + if (!placed) { 344 + bestPos = { 345 + x: current.x + prevDir.dx * STEP_SIZE, 346 + y: current.y + prevDir.dy * STEP_SIZE, 347 + z: current.z + prevDir.dz * STEP_SIZE 348 + }; 349 + bestDir = prevDir; 350 + } 321 351 322 - placedInCluster.add(target.domain); 323 - placedStars.push(target); 324 - directions.set(target.domain, bestDir); 325 - layoutQueue.push(target); 326 - } 327 - } 352 + target.x = bestPos.x; 353 + target.y = bestPos.y; 354 + target.z = bestPos.z; 328 355 329 - // Handle isolated/leftover stars 330 - for (const star of cluster.stars) { 331 - if (!placedInCluster.has(star.domain)) { 332 - // Try random spots near center 333 - for (let k = 0; k < 10; k++) { 334 - const cx = cluster.center.x + (rng.next() - 0.5) * 200; 335 - const cy = cluster.center.y + (rng.next() - 0.5) * 200; 336 - const cz = cluster.center.z + (rng.next() - 0.5) * 200; 356 + placedInCluster.add(target.domain); 357 + placedStars.push(target); 358 + directions.set(target.domain, bestDir); 359 + layoutQueue.push(target); 360 + } 361 + } 337 362 338 - if (!checkCollision(cx, cy, cz) || k === 9) { 339 - star.x = cx; star.y = cy; star.z = cz; 340 - break; 341 - } 342 - } 343 - placedInCluster.add(star.domain); 344 - placedStars.push(star); 345 - } 346 - } 347 - } 363 + // Handle isolated/leftover stars 364 + for (const star of cluster.stars) { 365 + if (!placedInCluster.has(star.domain)) { 366 + // Try random spots near center 367 + for (let k = 0; k < 10; k++) { 368 + const cx = cluster.center.x + (rng.next() - 0.5) * 200; 369 + const cy = cluster.center.y + (rng.next() - 0.5) * 200; 370 + const cz = cluster.center.z + (rng.next() - 0.5) * 200; 348 371 349 - // 5. Generate Nebulae (Density-based) 350 - const PROBE_COUNT = 300; 351 - const SEARCH_RADIUS = 400; 352 - const DENSITY_THRESHOLD = 4; 353 - type NebulaDef = { x: number, y: number, z: number, density: number }; 354 - let candidates: NebulaDef[] = []; 372 + if (!checkCollision(cx, cy, cz) || k === 9) { 373 + star.x = cx; 374 + star.y = cy; 375 + star.z = cz; 376 + break; 377 + } 378 + } 379 + placedInCluster.add(star.domain); 380 + placedStars.push(star); 381 + } 382 + } 383 + } 355 384 356 - for (let i = 0; i < PROBE_COUNT; i++) { 357 - if (stars.length === 0) break; 358 - const p = stars[Math.floor(rng.next() * stars.length)]; 385 + // 5. Generate Nebulae (Density-based) 386 + const PROBE_COUNT = 300; 387 + const SEARCH_RADIUS = 400; 388 + const DENSITY_THRESHOLD = 4; 389 + type NebulaDef = { x: number; y: number; z: number; density: number }; 390 + let candidates: NebulaDef[] = []; 359 391 360 - let neighbors = 0; 361 - let sumX = 0, sumY = 0, sumZ = 0; 392 + for (let i = 0; i < PROBE_COUNT; i++) { 393 + if (stars.length === 0) break; 394 + const p = stars[Math.floor(rng.next() * stars.length)]; 362 395 363 - for (const s of stars) { 364 - const dx = s.x - p.x; 365 - const dy = s.y - p.y; 366 - const dz = s.z - p.z; 367 - const d2 = dx * dx + dy * dy + dz * dz; 368 - if (d2 < SEARCH_RADIUS * SEARCH_RADIUS) { 369 - neighbors++; 370 - sumX += s.x; 371 - sumY += s.y; 372 - sumZ += s.z; 373 - } 374 - } 396 + let neighbors = 0; 397 + let sumX = 0, 398 + sumY = 0, 399 + sumZ = 0; 375 400 376 - if (neighbors >= DENSITY_THRESHOLD) 377 - candidates.push({ 378 - x: sumX / neighbors, 379 - y: sumY / neighbors, 380 - z: sumZ / neighbors, 381 - density: neighbors 382 - }); 401 + for (const s of stars) { 402 + const dx = s.x - p.x; 403 + const dy = s.y - p.y; 404 + const dz = s.z - p.z; 405 + const d2 = dx * dx + dy * dy + dz * dz; 406 + if (d2 < SEARCH_RADIUS * SEARCH_RADIUS) { 407 + neighbors++; 408 + sumX += s.x; 409 + sumY += s.y; 410 + sumZ += s.z; 411 + } 412 + } 383 413 384 - } 414 + if (neighbors >= DENSITY_THRESHOLD) 415 + candidates.push({ 416 + x: sumX / neighbors, 417 + y: sumY / neighbors, 418 + z: sumZ / neighbors, 419 + density: neighbors 420 + }); 421 + } 385 422 386 - const nebulae: Nebula[] = []; 387 - const MERGE_DIST = 400; 388 - candidates.sort((a, b) => b.density - a.density); 423 + const nebulae: Nebula[] = []; 424 + const MERGE_DIST = 400; 425 + candidates.sort((a, b) => b.density - a.density); 389 426 390 - console.log(`found ${candidates.length} density-based nebula candidates.`); 427 + console.log(`found ${candidates.length} density-based nebula candidates.`); 391 428 392 - const breakDensity = candidates[Math.floor(candidates.length * 0.7)].density; 393 - console.log(`70th percentile density: ${breakDensity}`); 429 + const breakDensity = candidates[Math.floor(candidates.length * 0.7)].density; 430 + console.log(`70th percentile density: ${breakDensity}`); 394 431 395 - for (const c of candidates) { 396 - let merged = false; 397 - for (const n of nebulae) { 398 - const dist = Math.sqrt((c.x - n.x) ** 2 + (c.y - n.y) ** 2 + (c.z - n.z) ** 2); 399 - if (dist < MERGE_DIST) { 400 - n.density = Math.max(n.density, c.density); 401 - merged = true; 402 - break; 403 - } 404 - } 405 - if (!merged) nebulae.push(c); 432 + for (const c of candidates) { 433 + let merged = false; 434 + for (const n of nebulae) { 435 + const dist = Math.sqrt((c.x - n.x) ** 2 + (c.y - n.y) ** 2 + (c.z - n.z) ** 2); 436 + if (dist < MERGE_DIST) { 437 + n.density = Math.max(n.density, c.density); 438 + merged = true; 439 + break; 440 + } 441 + } 442 + if (!merged) nebulae.push(c); 406 443 407 - if (c.density < breakDensity) break; 408 - } 409 - console.log(`generated ${nebulae.length} density-based nebulae.`); 444 + if (c.density < breakDensity) break; 445 + } 446 + console.log(`generated ${nebulae.length} density-based nebulae.`); 410 447 411 - // 6. Generate Dust (Void Noise) 412 - const dust: Dust[] = []; 413 - const DUST_COUNT = 50000; 414 - console.log('generating void noise...'); 448 + // 6. Generate Dust (Void Noise) 449 + const dust: Dust[] = []; 450 + const DUST_COUNT = 50000; 451 + console.log('generating void noise...'); 415 452 416 - for (let i = 0; i < DUST_COUNT; i++) { 417 - const u = rng.next(); 418 - const v = rng.next(); 419 - const theta = 2 * Math.PI * u; 420 - const phi = Math.acos(2 * v - 1); 421 - const r = sphereRadius * Math.cbrt(rng.next()); 453 + for (let i = 0; i < DUST_COUNT; i++) { 454 + const u = rng.next(); 455 + const v = rng.next(); 456 + const theta = 2 * Math.PI * u; 457 + const phi = Math.acos(2 * v - 1); 458 + const r = sphereRadius * Math.cbrt(rng.next()); 422 459 423 - const dx = r * Math.sin(phi) * Math.cos(theta); 424 - const dy = r * Math.sin(phi) * Math.sin(theta); 425 - const dz = r * Math.cos(phi); 460 + const dx = r * Math.sin(phi) * Math.cos(theta); 461 + const dy = r * Math.sin(phi) * Math.sin(theta); 462 + const dz = r * Math.cos(phi); 426 463 427 - const baseAlpha = 0.15 + rng.next() * 0.3; 464 + const baseAlpha = 0.15 + rng.next() * 0.3; 428 465 429 - dust.push({ 430 - x: dx, 431 - y: dy, 432 - z: dz, 433 - alpha: baseAlpha, 434 - sizeFactor: (0.5 + rng.next() * 1.5), 435 - color: rng.next() > 0.5 ? '#FFFFFF' : '#AAAAAA' 436 - }); 437 - } 438 - console.log(`generated ${dust.length} dust particles.`); 466 + dust.push({ 467 + x: dx, 468 + y: dy, 469 + z: dz, 470 + alpha: baseAlpha, 471 + sizeFactor: 0.5 + rng.next() * 1.5, 472 + color: rng.next() > 0.5 ? '#FFFFFF' : '#AAAAAA' 473 + }); 474 + } 475 + console.log(`generated ${dust.length} dust particles.`); 439 476 440 - return { stars, nebulae, dust }; 441 - } 477 + return { stars, nebulae, dust }; 478 + }; 442 479 443 480 export const initConstellation = async () => { 444 - try { 445 - try { 446 - await stat(DATA_DIR); 447 - } catch { 448 - await mkdir(DATA_DIR, { recursive: true }); 449 - } 481 + try { 482 + try { 483 + await stat(DATA_DIR); 484 + } catch { 485 + await mkdir(DATA_DIR, { recursive: true }); 486 + } 450 487 451 - let start = Date.now(); 452 - console.log('fetching 88x31s graph data...'); 453 - const response = await fetch(GRAPH_URL); 454 - const data: GraphData = await response.json(); 455 - console.log(`fetched 88x31s graph data in ${Date.now() - start}ms`); 488 + sharp.concurrency(1); 456 489 457 - start = Date.now(); 458 - console.log('generating constellation data...'); 459 - const { stars, nebulae, dust } = generateConstellationData(data); 490 + let start = Date.now(); 491 + console.log('fetching 88x31s graph data...'); 492 + const response = await fetch(GRAPH_URL); 493 + const data: GraphData = await response.json(); 494 + console.log(`fetched 88x31s graph data in ${Date.now() - start}ms`); 460 495 461 - await writeFile(GRAPH_FILE, JSON.stringify({ stars, nebulae, dust })); 462 - console.log(`${stars.length} stars, ${nebulae.length} nebulae, ${dust.length} dust particles generated in ${Date.now() - start}ms`); 496 + start = Date.now(); 497 + console.log('generating constellation data...'); 498 + const { stars, nebulae, dust } = generateConstellationData(data); 499 + 500 + await writeFile(GRAPH_FILE, JSON.stringify({ stars, nebulae, dust })); 501 + console.log( 502 + `${stars.length} stars, ${nebulae.length} nebulae, ${dust.length} dust particles generated in ${Date.now() - start}ms` 503 + ); 463 504 464 - await renderConstellation(); 465 - } catch (error) { 466 - console.error('error initializing constellation:', error); 467 - } 468 - } 505 + await renderConstellation(); 506 + } catch (error) { 507 + console.error('error initializing constellation:', error); 508 + } 509 + }; 469 510 470 - type ProjectedTrans = { x: number, y: number, scale: number, z: number }; 511 + type ProjectedTrans = { x: number; y: number; scale: number; z: number }; 471 512 472 513 export const renderConstellation = async () => { 473 - try { 474 - try { 475 - await stat(GRAPH_FILE); 476 - } catch { 477 - await initConstellation(); 478 - return; 479 - } 514 + try { 515 + try { 516 + await stat(GRAPH_FILE); 517 + } catch { 518 + await initConstellation(); 519 + return; 520 + } 480 521 481 - const start = Date.now(); 482 - console.log('rendering constellation to SVG...'); 522 + const start = Date.now(); 523 + console.log('rendering constellation to SVG...'); 483 524 484 - const constellationData: ConstellationData = JSON.parse(await readFile(GRAPH_FILE, 'utf-8')); 485 - const { stars, nebulae, dust } = constellationData; 525 + const constellationData: ConstellationData = JSON.parse(await readFile(GRAPH_FILE, 'utf-8')); 526 + const { stars, nebulae, dust } = constellationData; 486 527 487 - const RESOLUTION_SCALE = 1; 488 - const width = 1920 * RESOLUTION_SCALE; 489 - const height = 1080 * RESOLUTION_SCALE; 528 + const RESOLUTION_SCALE = 1; 529 + const width = 1920 * RESOLUTION_SCALE; 530 + const height = 1080 * RESOLUTION_SCALE; 490 531 491 - const fov = 400 * RESOLUTION_SCALE; // Field of view equivalent 492 - const cx = width / 2; 493 - const cy = height / 2; 532 + const fov = 400 * RESOLUTION_SCALE; // Field of view equivalent 533 + const cx = width / 2; 534 + const cy = height / 2; 494 535 495 - // Calculate angle based on time: one full rotation per 3 hours (Y-axis) 496 - const periodY = 3 * 60 * 60 * 1000; 497 - // Secondary rotation on X-axis to see the poles (different period to avoid repeating patterns) 498 - const periodX = 5.14 * 60 * 60 * 1000; 536 + // Calculate angle based on time: one full rotation per 3 hours (Y-axis) 537 + const periodY = 3 * 60 * 60 * 1000; 538 + // Secondary rotation on X-axis to see the poles (different period to avoid repeating patterns) 539 + const periodX = 5.14 * 60 * 60 * 1000; 499 540 500 - const date = Date.now(); 501 - const angleY = ((date % periodY) / periodY) * Math.PI * 2; 502 - const angleX = ((date % periodX) / periodX) * Math.PI * 2; 541 + const date = Date.now(); 542 + const angleY = ((date % periodY) / periodY) * Math.PI * 2; 543 + const angleX = ((date % periodX) / periodX) * Math.PI * 2; 503 544 504 - const cosY = Math.cos(angleY); 505 - const sinY = Math.sin(angleY); 506 - const cosX = Math.cos(angleX); 507 - const sinX = Math.sin(angleX); 545 + const cosY = Math.cos(angleY); 546 + const sinY = Math.sin(angleY); 547 + const cosX = Math.cos(angleX); 548 + const sinX = Math.sin(angleX); 508 549 509 - const rotatePoint = (x: number, y: number, z: number) => { 510 - // Yaw (Y-axis) 511 - const x1 = x * cosY - z * sinY; 512 - const z1 = z * cosY + x * sinY; 513 - const y1 = y; 550 + const rotatePoint = (x: number, y: number, z: number) => { 551 + // Yaw (Y-axis) 552 + const x1 = x * cosY - z * sinY; 553 + const z1 = z * cosY + x * sinY; 554 + const y1 = y; 514 555 515 - // Pitch (X-axis) 516 - const y2 = y1 * cosX - z1 * sinX; 517 - const z2 = z1 * cosX + y1 * sinX; 518 - const x2 = x1; 556 + // Pitch (X-axis) 557 + const y2 = y1 * cosX - z1 * sinX; 558 + const z2 = z1 * cosX + y1 * sinX; 559 + const x2 = x1; 519 560 520 - return { x: x2, y: y2, z: z2 }; 521 - }; 561 + return { x: x2, y: y2, z: z2 }; 562 + }; 522 563 523 - let svgBody = ''; 524 - let defsContent = ''; 564 + let svgBody = ''; 565 + let defsContent = ''; 525 566 526 - const stage = new Konva.Stage({ 527 - width, 528 - height, 529 - }); 530 - const layer = new Konva.Layer(); 531 - stage.add(layer); 567 + const stage = new Konva.Stage({ 568 + width, 569 + height 570 + }); 571 + const layer = new Konva.Layer(); 572 + stage.add(layer); 532 573 533 - const rect = new Konva.Rect({ 534 - width, 535 - height, 536 - fill: '#000000', 537 - }); 538 - layer.add(rect); 574 + const rect = new Konva.Rect({ 575 + width, 576 + height, 577 + fill: '#000000' 578 + }); 579 + layer.add(rect); 539 580 540 - // Draw dust particles using Konva 541 - for (const d of dust) { 542 - const { x: rotX, y: rotY, z: rotZ } = rotatePoint(d.x, d.y, d.z); 581 + // Draw dust particles using Konva 582 + for (const d of dust) { 583 + const { x: rotX, y: rotY, z: rotZ } = rotatePoint(d.x, d.y, d.z); 543 584 544 - if (rotZ > 100) { 545 - const scale = fov / rotZ; 546 - const screenX = cx + rotX * scale; 547 - const screenY = cy + (rotY * scale * -1); 585 + if (rotZ > 100) { 586 + const scale = fov / rotZ; 587 + const screenX = cx + rotX * scale; 588 + const screenY = cy + rotY * scale * -1; 548 589 549 - const size = d.sizeFactor * scale; 590 + const size = d.sizeFactor * scale; 550 591 551 - const rect = new Konva.Rect({ 552 - x: screenX, 553 - y: screenY, 554 - width: size, 555 - height: size, 556 - fill: d.color, 557 - opacity: d.alpha, 558 - }); 559 - layer.add(rect); 560 - } 561 - } 592 + const rect = new Konva.Rect({ 593 + x: screenX, 594 + y: screenY, 595 + width: size, 596 + height: size, 597 + fill: d.color, 598 + opacity: d.alpha 599 + }); 600 + layer.add(rect); 601 + } 602 + } 562 603 563 - layer.draw(); 604 + layer.draw(); 564 605 565 - const sharpImg = await (stage.toCanvas() as unknown as Canvas).toSharp(); 566 - const buffer = await sharpImg.webp({ effort: 6, quality: 30, smartDeblock: true }).toBuffer(); 567 - await writeFile(DUST_FILE, buffer); 606 + const sharpImg = await (stage.toCanvas() as unknown as Canvas).toSharp(); 607 + const buffer = await sharpImg.webp({ effort: 6, quality: 30, smartDeblock: true }).toBuffer(); 608 + await writeFile(DUST_FILE, buffer); 568 609 569 - const projected: Record<string, ProjectedTrans> = {}; 610 + const projected: Record<string, ProjectedTrans> = {}; 570 611 571 - const fmt = (n: number) => n.toFixed(2); // Round to 2 decimal places 612 + const fmt = (n: number) => n.toFixed(2); // Round to 2 decimal places 572 613 573 - // 0. Universe Noise / Heatmap (Background Nebulae) 574 - let nebulaIndex = 0; 575 - for (const n of nebulae) { 576 - // Rotate matches star rotation 577 - const { x: rotX, y: rotY, z: rotZ } = rotatePoint(n.x, n.y, n.z); 614 + // 0. Universe Noise / Heatmap (Background Nebulae) 615 + let nebulaIndex = 0; 616 + for (const n of nebulae) { 617 + // Rotate matches star rotation 618 + const { x: rotX, y: rotY, z: rotZ } = rotatePoint(n.x, n.y, n.z); 578 619 579 - // Render if in front of camera 580 - if (rotZ > 100) { 581 - const scale = fov / rotZ; 582 - const screenX = cx + rotX * scale; 583 - const screenY = cy + (rotY * scale * -1); 620 + // Render if in front of camera 621 + if (rotZ > 100) { 622 + const scale = fov / rotZ; 623 + const screenX = cx + rotX * scale; 624 + const screenY = cy + rotY * scale * -1; 584 625 585 - // Density -> Size & Opacity 586 - const intensity = Math.min(1, n.density / 25); 626 + // Density -> Size & Opacity 627 + const intensity = Math.min(1, n.density / 25); 587 628 588 - const hueSeed = Math.abs(Math.sin(n.x * n.y * n.z)); 589 - const hue = 200 + hueSeed * 80; 629 + const hueSeed = Math.abs(Math.sin(n.x * n.y * n.z)); 630 + const hue = 200 + hueSeed * 80; 590 631 591 - const radius = (600 + intensity * 800) * scale; 592 - const alpha = 0.2 + intensity * 0.5; 632 + const radius = (600 + intensity * 800) * scale; 633 + const alpha = 0.2 + intensity * 0.5; 593 634 594 - const gradId = `nebula-${nebulaIndex++}`; 595 - defsContent += ` 635 + const gradId = `nebula-${nebulaIndex++}`; 636 + defsContent += ` 596 637 <radialGradient id="${gradId}" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"> 597 638 <stop offset="0%" stop-color="hsla(${fmt(hue)}, 90%, 60%, ${fmt(alpha)})" /> 598 639 <stop offset="100%" stop-color="hsla(0, 0%, 0%, 0)" /> 599 640 </radialGradient>`; 600 641 601 - svgBody += `<circle cx="${fmt(screenX)}" cy="${fmt(screenY)}" r="${fmt(radius)}" fill="url(#${gradId})" opacity="1" />`; 602 - } 603 - } 642 + svgBody += `<circle cx="${fmt(screenX)}" cy="${fmt(screenY)}" r="${fmt(radius)}" fill="url(#${gradId})" opacity="1" />`; 643 + } 644 + } 604 645 605 - // 1. Projection pass 606 - for (const star of stars) { 607 - const { x: rotX, y: rotY, z: rotZ } = rotatePoint(star.x, star.y, star.z); 646 + // 1. Projection pass 647 + for (const star of stars) { 648 + const { x: rotX, y: rotY, z: rotZ } = rotatePoint(star.x, star.y, star.z); 608 649 609 - if (rotZ > 10) { 610 - const scale = fov / rotZ; 611 - const screenX = cx + rotX * scale; 612 - const screenY = cy + (rotY * scale * -1); 650 + if (rotZ > 10) { 651 + const scale = fov / rotZ; 652 + const screenX = cx + rotX * scale; 653 + const screenY = cy + rotY * scale * -1; 613 654 614 - projected[star.domain] = { x: screenX, y: screenY, scale, z: rotZ }; 615 - } 616 - } 655 + projected[star.domain] = { x: screenX, y: screenY, scale, z: rotZ }; 656 + } 657 + } 617 658 618 - // 2. Draw connections 619 - const drawnConnections = new Set<string>(); 659 + // 2. Draw connections 660 + const drawnConnections = new Set<string>(); 620 661 621 - type RenderLine = { 622 - p1: { x: number, y: number, z: number }; 623 - p2: { x: number, y: number, z: number }; 624 - avgZ: number; 625 - }; 662 + type RenderLine = { 663 + p1: { x: number; y: number; z: number }; 664 + p2: { x: number; y: number; z: number }; 665 + avgZ: number; 666 + }; 626 667 627 - const linesToDraw: RenderLine[] = []; 668 + const linesToDraw: RenderLine[] = []; 628 669 629 - for (const star of stars) { 630 - if (!projected[star.domain]) continue; 670 + for (const star of stars) { 671 + if (!projected[star.domain]) continue; 631 672 632 - const p1 = projected[star.domain]; 673 + const p1 = projected[star.domain]; 633 674 634 - if (star.visualConnections) { 635 - for (const target of star.visualConnections) { 636 - const key = [star.domain, target].sort().join('-'); 637 - if (drawnConnections.has(key)) continue; 638 - drawnConnections.add(key); 675 + if (star.visualConnections) { 676 + for (const target of star.visualConnections) { 677 + const key = [star.domain, target].sort().join('-'); 678 + if (drawnConnections.has(key)) continue; 679 + drawnConnections.add(key); 639 680 640 - if (projected[target]) { 641 - const p2 = projected[target]; 642 - const avgZ = (p1.z + p2.z) / 2; 643 - linesToDraw.push({ p1, p2, avgZ }); 644 - } 645 - } 646 - } 647 - } 681 + if (projected[target]) { 682 + const p2 = projected[target]; 683 + const avgZ = (p1.z + p2.z) / 2; 684 + linesToDraw.push({ p1, p2, avgZ }); 685 + } 686 + } 687 + } 688 + } 648 689 649 - linesToDraw.sort((a, b) => b.avgZ - a.avgZ); 690 + linesToDraw.sort((a, b) => b.avgZ - a.avgZ); 650 691 651 - for (const line of linesToDraw) { 652 - const { p1, p2, avgZ } = line; 692 + for (const line of linesToDraw) { 693 + const { p1, p2, avgZ } = line; 653 694 654 - const opacity = Math.max(0.4, Math.min(1, 1 - (avgZ / 3000))); 655 - const strokeWidth = Math.max(0.2 * RESOLUTION_SCALE, 1.5 * RESOLUTION_SCALE * (1000 / avgZ)); 695 + const opacity = Math.max(0.4, Math.min(1, 1 - avgZ / 3000)); 696 + const strokeWidth = Math.max(0.2 * RESOLUTION_SCALE, 1.5 * RESOLUTION_SCALE * (1000 / avgZ)); 656 697 657 - // Halo (black line behind) 658 - // svgBody += `<line x1="${p1.x}" y1="${p1.y}" x2="${p2.x}" y2="${p2.y}" stroke="#000000" stroke-width="${strokeWidth + strokeWidth * opacity}" opacity="1" stroke-linecap="butt" />`; 698 + // Halo (black line behind) 699 + // svgBody += `<line x1="${p1.x}" y1="${p1.y}" x2="${p2.x}" y2="${p2.y}" stroke="#000000" stroke-width="${strokeWidth + strokeWidth * opacity}" opacity="1" stroke-linecap="butt" />`; 659 700 660 - // Actual Line 661 - svgBody += `<line x1="${fmt(p1.x)}" y1="${fmt(p1.y)}" x2="${fmt(p2.x)}" y2="${fmt(p2.y)}" stroke="#FFFFFF" stroke-width="${fmt(strokeWidth)}" opacity="${fmt(opacity)}" stroke-linecap="butt" />`; 662 - } 701 + // Actual Line 702 + svgBody += `<line x1="${fmt(p1.x)}" y1="${fmt(p1.y)}" x2="${fmt(p2.x)}" y2="${fmt(p2.y)}" stroke="#FFFFFF" stroke-width="${fmt(strokeWidth)}" opacity="${fmt(opacity)}" stroke-linecap="butt" />`; 703 + } 663 704 664 - // for interactivity (we put links on these screen coordinates) 665 - const visibleStars: { domain: string, x: number, y: number, r: number }[] = []; 666 - // 3. Draw Stars 667 - for (const star of stars) { 668 - if (!projected[star.domain]) continue; 669 - const p = projected[star.domain]; 705 + // for interactivity (we put links on these screen coordinates) 706 + const visibleStars: { domain: string; x: number; y: number; r: number }[] = []; 707 + // 3. Draw Stars 708 + for (const star of stars) { 709 + if (!projected[star.domain]) continue; 710 + const p = projected[star.domain]; 670 711 671 - const connectionCount = star.connections ? star.connections.length : 0; 672 - const importance = Math.min(1.5, 1 + connectionCount * 0.1); 712 + const connectionCount = star.connections ? star.connections.length : 0; 713 + const importance = Math.min(1.5, 1 + connectionCount * 0.1); 673 714 674 - const radius = Math.max(1 * RESOLUTION_SCALE, 25 * p.scale * importance) * 0.4; 675 - const haloRadius = radius * 1.85; 715 + const radius = Math.max(1 * RESOLUTION_SCALE, 25 * p.scale * importance) * 0.4; 716 + const haloRadius = radius * 1.85; 676 717 677 - const strokeWidth = haloRadius - radius; 718 + const strokeWidth = haloRadius - radius; 678 719 679 - const opacity = Math.min(1, Math.max(0.2, 1000 / p.z)); 680 - const haloOpacity = opacity * 0.3; 720 + const opacity = Math.min(1, Math.max(0.2, 1000 / p.z)); 721 + const haloOpacity = opacity * 0.3; 681 722 682 - svgBody += `<rect x="${fmt(p.x - radius / 2)}" y="${fmt(p.y - radius / 2)}" width="${fmt(radius)}" height="${fmt(radius)}" fill="#EEEEEE" fill-opacity="${fmt(opacity)}" stroke="#FFFFFF" stroke-opacity="${fmt(haloOpacity)}" stroke-width="${fmt(strokeWidth)}" paint-order="stroke fill" />`; 723 + svgBody += `<rect x="${fmt(p.x - radius / 2)}" y="${fmt(p.y - radius / 2)}" width="${fmt(radius)}" height="${fmt(radius)}" fill="#EEEEEE" fill-opacity="${fmt(opacity)}" stroke="#FFFFFF" stroke-opacity="${fmt(haloOpacity)}" stroke-width="${fmt(strokeWidth)}" paint-order="stroke fill" />`; 683 724 684 - visibleStars.push({ domain: star.domain, x: p.x, y: p.y, r: radius * 1.75 }); 685 - } 725 + visibleStars.push({ domain: star.domain, x: p.x, y: p.y, r: radius * 1.75 }); 726 + } 686 727 687 - const finalSvg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg"> 728 + const finalSvg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg"> 688 729 <defs>${defsContent}</defs> 689 730 ${svgBody} 690 731 </svg>`; 691 732 692 - await writeFile(OUTPUT_FILE, finalSvg); 733 + await writeFile(OUTPUT_FILE, finalSvg); 693 734 694 - await writeFile(STARS_FILE, JSON.stringify({ 695 - width, 696 - height, 697 - stars: visibleStars, 698 - meta: { 699 - timestamp: new Date().toISOString(), 700 - angleY, 701 - angleX 702 - } 703 - })); 735 + await writeFile( 736 + STARS_FILE, 737 + JSON.stringify({ 738 + width, 739 + height, 740 + stars: visibleStars, 741 + meta: { 742 + timestamp: new Date().toISOString(), 743 + angleY, 744 + angleX 745 + } 746 + }) 747 + ); 704 748 705 - console.log('generating OG image...'); 706 - (async () => { 707 - const h = 630; 708 - const resized_svg = await sharp(OUTPUT_FILE).resize({ height: h }).toBuffer(); 709 - sharp(DUST_FILE) 710 - .resize({ height: h }) 711 - .composite([{ input: resized_svg }]) 712 - .png() 713 - .toFile(OG_IMAGE_FILE); 714 - })(); 749 + console.log('generating OG image...'); 750 + (async () => { 751 + const h = 630; 752 + const resized_svg = await sharp(OUTPUT_FILE).resize({ height: h }).toBuffer(); 753 + sharp(DUST_FILE) 754 + .resize({ height: h }) 755 + .composite([{ input: resized_svg }]) 756 + .png() 757 + .toFile(OG_IMAGE_FILE); 758 + })(); 715 759 716 - console.log(`rendered constellation in ${Date.now() - start}ms`); 717 - } catch (error) { 718 - console.error('error rendering constellation:', error); 719 - } 720 - } 760 + console.log(`rendered constellation in ${Date.now() - start}ms`); 761 + } catch (error) { 762 + console.error('error rendering constellation:', error); 763 + } 764 + };