data endpoint for entity 90008 (aka. a website)

optimize skyline bg image sizes

ptr.pet 8e31eb0d 444cc7fa

verified
+370 -162
+198
deno.lock
··· 29 29 "npm:prettier@^3.7.4": "3.7.4", 30 30 "npm:prometheus-remote-write@~0.5.1": "0.5.1_node-fetch@3.3.2", 31 31 "npm:robots-parser@^3.0.1": "3.0.1", 32 + "npm:sharp@~0.34.5": "0.34.5", 32 33 "npm:skia-canvas@^3.0.8": "3.0.8", 33 34 "npm:steamgriddb@^2.2.1": "2.2.1", 34 35 "npm:svelte-check@^4.3.5": "4.3.5_svelte@5.46.1__acorn@8.15.0_typescript@5.9.3", ··· 137 138 }, 138 139 "@darkvisitors/sdk@1.6.0": { 139 140 "integrity": "sha512-KfAO7Dzg/EGZGNRUVpjK8dBzOe9xQI2bXF9aq8JcEk8BiOIAn+e4Vf+ceVMhOBB8PkuLvRBnJwfADAYXNL0iFg==" 141 + }, 142 + "@emnapi/runtime@1.7.1": { 143 + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", 144 + "dependencies": [ 145 + "tslib" 146 + ] 140 147 }, 141 148 "@esbuild/aix-ppc64@0.27.2": { 142 149 "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", ··· 340 347 }, 341 348 "@humanwhocodes/retry@0.4.3": { 342 349 "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" 350 + }, 351 + "@img/colour@1.0.0": { 352 + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==" 353 + }, 354 + "@img/sharp-darwin-arm64@0.34.5": { 355 + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", 356 + "optionalDependencies": [ 357 + "@img/sharp-libvips-darwin-arm64" 358 + ], 359 + "os": ["darwin"], 360 + "cpu": ["arm64"] 361 + }, 362 + "@img/sharp-darwin-x64@0.34.5": { 363 + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", 364 + "optionalDependencies": [ 365 + "@img/sharp-libvips-darwin-x64" 366 + ], 367 + "os": ["darwin"], 368 + "cpu": ["x64"] 369 + }, 370 + "@img/sharp-libvips-darwin-arm64@1.2.4": { 371 + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", 372 + "os": ["darwin"], 373 + "cpu": ["arm64"] 374 + }, 375 + "@img/sharp-libvips-darwin-x64@1.2.4": { 376 + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", 377 + "os": ["darwin"], 378 + "cpu": ["x64"] 379 + }, 380 + "@img/sharp-libvips-linux-arm64@1.2.4": { 381 + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", 382 + "os": ["linux"], 383 + "cpu": ["arm64"] 384 + }, 385 + "@img/sharp-libvips-linux-arm@1.2.4": { 386 + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", 387 + "os": ["linux"], 388 + "cpu": ["arm"] 389 + }, 390 + "@img/sharp-libvips-linux-ppc64@1.2.4": { 391 + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", 392 + "os": ["linux"], 393 + "cpu": ["ppc64"] 394 + }, 395 + "@img/sharp-libvips-linux-riscv64@1.2.4": { 396 + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", 397 + "os": ["linux"], 398 + "cpu": ["riscv64"] 399 + }, 400 + "@img/sharp-libvips-linux-s390x@1.2.4": { 401 + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", 402 + "os": ["linux"], 403 + "cpu": ["s390x"] 404 + }, 405 + "@img/sharp-libvips-linux-x64@1.2.4": { 406 + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", 407 + "os": ["linux"], 408 + "cpu": ["x64"] 409 + }, 410 + "@img/sharp-libvips-linuxmusl-arm64@1.2.4": { 411 + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", 412 + "os": ["linux"], 413 + "cpu": ["arm64"] 414 + }, 415 + "@img/sharp-libvips-linuxmusl-x64@1.2.4": { 416 + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", 417 + "os": ["linux"], 418 + "cpu": ["x64"] 419 + }, 420 + "@img/sharp-linux-arm64@0.34.5": { 421 + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", 422 + "optionalDependencies": [ 423 + "@img/sharp-libvips-linux-arm64" 424 + ], 425 + "os": ["linux"], 426 + "cpu": ["arm64"] 427 + }, 428 + "@img/sharp-linux-arm@0.34.5": { 429 + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", 430 + "optionalDependencies": [ 431 + "@img/sharp-libvips-linux-arm" 432 + ], 433 + "os": ["linux"], 434 + "cpu": ["arm"] 435 + }, 436 + "@img/sharp-linux-ppc64@0.34.5": { 437 + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", 438 + "optionalDependencies": [ 439 + "@img/sharp-libvips-linux-ppc64" 440 + ], 441 + "os": ["linux"], 442 + "cpu": ["ppc64"] 443 + }, 444 + "@img/sharp-linux-riscv64@0.34.5": { 445 + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", 446 + "optionalDependencies": [ 447 + "@img/sharp-libvips-linux-riscv64" 448 + ], 449 + "os": ["linux"], 450 + "cpu": ["riscv64"] 451 + }, 452 + "@img/sharp-linux-s390x@0.34.5": { 453 + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", 454 + "optionalDependencies": [ 455 + "@img/sharp-libvips-linux-s390x" 456 + ], 457 + "os": ["linux"], 458 + "cpu": ["s390x"] 459 + }, 460 + "@img/sharp-linux-x64@0.34.5": { 461 + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", 462 + "optionalDependencies": [ 463 + "@img/sharp-libvips-linux-x64" 464 + ], 465 + "os": ["linux"], 466 + "cpu": ["x64"] 467 + }, 468 + "@img/sharp-linuxmusl-arm64@0.34.5": { 469 + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", 470 + "optionalDependencies": [ 471 + "@img/sharp-libvips-linuxmusl-arm64" 472 + ], 473 + "os": ["linux"], 474 + "cpu": ["arm64"] 475 + }, 476 + "@img/sharp-linuxmusl-x64@0.34.5": { 477 + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", 478 + "optionalDependencies": [ 479 + "@img/sharp-libvips-linuxmusl-x64" 480 + ], 481 + "os": ["linux"], 482 + "cpu": ["x64"] 483 + }, 484 + "@img/sharp-wasm32@0.34.5": { 485 + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", 486 + "dependencies": [ 487 + "@emnapi/runtime" 488 + ], 489 + "cpu": ["wasm32"] 490 + }, 491 + "@img/sharp-win32-arm64@0.34.5": { 492 + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", 493 + "os": ["win32"], 494 + "cpu": ["arm64"] 495 + }, 496 + "@img/sharp-win32-ia32@0.34.5": { 497 + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", 498 + "os": ["win32"], 499 + "cpu": ["ia32"] 500 + }, 501 + "@img/sharp-win32-x64@0.34.5": { 502 + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", 503 + "os": ["win32"], 504 + "cpu": ["x64"] 343 505 }, 344 506 "@isaacs/ttlcache@1.4.1": { 345 507 "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==" ··· 2019 2181 "set-cookie-parser@2.7.2": { 2020 2182 "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" 2021 2183 }, 2184 + "sharp@0.34.5": { 2185 + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", 2186 + "dependencies": [ 2187 + "@img/colour", 2188 + "detect-libc", 2189 + "semver" 2190 + ], 2191 + "optionalDependencies": [ 2192 + "@img/sharp-darwin-arm64", 2193 + "@img/sharp-darwin-x64", 2194 + "@img/sharp-libvips-darwin-arm64", 2195 + "@img/sharp-libvips-darwin-x64", 2196 + "@img/sharp-libvips-linux-arm", 2197 + "@img/sharp-libvips-linux-arm64", 2198 + "@img/sharp-libvips-linux-ppc64", 2199 + "@img/sharp-libvips-linux-riscv64", 2200 + "@img/sharp-libvips-linux-s390x", 2201 + "@img/sharp-libvips-linux-x64", 2202 + "@img/sharp-libvips-linuxmusl-arm64", 2203 + "@img/sharp-libvips-linuxmusl-x64", 2204 + "@img/sharp-linux-arm", 2205 + "@img/sharp-linux-arm64", 2206 + "@img/sharp-linux-ppc64", 2207 + "@img/sharp-linux-riscv64", 2208 + "@img/sharp-linux-s390x", 2209 + "@img/sharp-linux-x64", 2210 + "@img/sharp-linuxmusl-arm64", 2211 + "@img/sharp-linuxmusl-x64", 2212 + "@img/sharp-wasm32", 2213 + "@img/sharp-win32-arm64", 2214 + "@img/sharp-win32-ia32", 2215 + "@img/sharp-win32-x64" 2216 + ], 2217 + "scripts": true 2218 + }, 2022 2219 "shebang-command@2.0.0": { 2023 2220 "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 2024 2221 "dependencies": [ ··· 2403 2600 "npm:prettier@^3.7.4", 2404 2601 "npm:prometheus-remote-write@~0.5.1", 2405 2602 "npm:robots-parser@^3.0.1", 2603 + "npm:sharp@~0.34.5", 2406 2604 "npm:skia-canvas@^3.0.8", 2407 2605 "npm:steamgriddb@^2.2.1", 2408 2606 "npm:svelte-check@^4.3.5",
+1
eunomia/package.json
··· 47 47 "node-fetch": "^3.3.2", 48 48 "prometheus-remote-write": "^0.5.1", 49 49 "robots-parser": "^3.0.1", 50 + "sharp": "^0.34.5", 50 51 "steamgriddb": "^2.2.1", 51 52 "toad-scheduler": "^3.1.0", 52 53 "konva": "^10.0.12",
+94 -149
eunomia/src/lib/constellation.ts
··· 1 1 import Konva from 'konva'; 2 2 import 'konva/skia-backend'; 3 - import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'; 3 + import { writeFile, readFile, stat, mkdir } from 'node:fs/promises'; 4 4 import { join } from 'node:path'; 5 5 import { env } from '$env/dynamic/private'; 6 + import type { Canvas } from 'skia-canvas'; 6 7 7 8 const DATA_DIR = join(env.WEBSITE_DATA_DIR, 'constellation'); 8 9 const GRAPH_FILE = join(DATA_DIR, 'graph_processed.json'); 9 - const OUTPUT_FILE = join(DATA_DIR, 'background.png'); 10 + const OUTPUT_FILE = join(DATA_DIR, 'background.svg'); 11 + const DUST_FILE = join(DATA_DIR, 'background_dust.webp'); 10 12 const STARS_FILE = join(DATA_DIR, 'stars.json'); 11 13 const GRAPH_URL = 'https://eightyeightthirty.one/graph.json'; 12 14 ··· 420 422 const dy = r * Math.sin(phi) * Math.sin(theta); 421 423 const dz = r * Math.cos(phi); 422 424 423 - // Uniform distribution, no avoidance 424 - const baseAlpha = 0.1 + rng.next() * 0.2; 425 + const baseAlpha = 0.15 + rng.next() * 0.3; 425 426 426 427 dust.push({ 427 428 x: dx, ··· 439 440 440 441 export const initConstellation = async () => { 441 442 try { 442 - mkdirSync(DATA_DIR, { recursive: true }); 443 + try { 444 + await stat(DATA_DIR); 445 + } catch { 446 + await mkdir(DATA_DIR, { recursive: true }); 447 + } 443 448 444 449 let start = Date.now(); 445 450 console.log('fetching 88x31s graph data...'); ··· 451 456 console.log('generating constellation data...'); 452 457 const { stars, nebulae, dust } = generateConstellationData(data); 453 458 454 - // Save legacy map for stars if strictly needed, but better to save the whole object 455 - // The render function needs the whole object now. 456 - // We'll save the structure exactly as ConstellationData 457 - 458 - writeFileSync(GRAPH_FILE, JSON.stringify({ stars, nebulae, dust })); 459 + await writeFile(GRAPH_FILE, JSON.stringify({ stars, nebulae, dust })); 459 460 console.log(`${stars.length} stars, ${nebulae.length} nebulae, ${dust.length} dust particles generated in ${Date.now() - start}ms`); 460 461 461 462 await renderConstellation(); ··· 468 469 469 470 export const renderConstellation = async () => { 470 471 try { 471 - if (!existsSync(GRAPH_FILE)) { 472 + try { 473 + await stat(GRAPH_FILE); 474 + } catch { 472 475 await initConstellation(); 473 476 return; 474 477 } 475 478 476 479 const start = Date.now(); 477 - console.log('rendering constellation...'); 480 + console.log('rendering constellation to SVG...'); 478 481 479 - const constellationData: ConstellationData = JSON.parse(readFileSync(GRAPH_FILE, 'utf-8')); 482 + const constellationData: ConstellationData = JSON.parse(await readFile(GRAPH_FILE, 'utf-8')); 480 483 const { stars, nebulae, dust } = constellationData; 481 484 482 - // Canvas setup 483 - const RESOLUTION_SCALE = 2; // 4K resolution 485 + const RESOLUTION_SCALE = 1; 484 486 const width = 1920 * RESOLUTION_SCALE; 485 487 const height = 1080 * RESOLUTION_SCALE; 486 488 487 - // Create stage and layer 488 - const stage = new Konva.Stage({ 489 - width, 490 - height, 491 - }); 492 - 493 - const layer = new Konva.Layer({ imageSmoothingEnabled: false }); 494 - stage.add(layer); 495 - 496 - // Background 497 - const rect = new Konva.Rect({ 498 - width, 499 - height, 500 - fill: '#000000', 501 - }); 502 - layer.add(rect); 503 - 504 489 const fov = 400 * RESOLUTION_SCALE; // Field of view equivalent 505 490 const cx = width / 2; 506 491 const cy = height / 2; ··· 534 519 return { x: x2, y: y2, z: z2 }; 535 520 }; 536 521 537 - // Project and draw 538 - const projected: Record<string, ProjectedTrans> = {}; 522 + // Initialize SVG content 523 + let svgBody = ''; 524 + let defsContent = ''; 539 525 540 - // 0. Universe Noise / Heatmap (Background Nebulae) 541 - // Draw this BEFORE everything else so it's in the background 526 + // 0.5 Render Dust via Konva 527 + // Use a Stage/Layer for dust only, transparent background 528 + const stage = new Konva.Stage({ 529 + width, 530 + height, 531 + }); 532 + const layer = new Konva.Layer(); 533 + stage.add(layer); 542 534 543 - // Algorithm: Find dense clusters of stars to place nebulae 544 - // 1. We'll use a simplified density estimation. 545 - // Pick N random 'probe' points (existing stars) and calculate how many neighbors they have within Radius R. 546 - // Higher neighbor count = higher density = larger/brighter nebula. 535 + const rect = new Konva.Rect({ 536 + width, 537 + height, 538 + fill: '#000000', 539 + }); 540 + layer.add(rect); 547 541 548 - for (const n of nebulae) { 549 - // Rotate matches star rotation 550 - const { x: rotX, y: rotY, z: rotZ } = rotatePoint(n.x, n.y, n.z); 542 + // Draw dust particles using Konva 543 + for (const d of dust) { 544 + const { x: rotX, y: rotY, z: rotZ } = rotatePoint(d.x, d.y, d.z); 551 545 552 - // Render if in front of camera 553 546 if (rotZ > 100) { 554 547 const scale = fov / rotZ; 555 548 const screenX = cx + rotX * scale; 556 549 const screenY = cy + (rotY * scale * -1); 557 550 558 - // Density -> Size & Opacity 559 - // Normalize density: typical range 5 to 50? 560 - const intensity = Math.min(1, n.density / 25); 551 + const size = d.sizeFactor * scale; 561 552 562 - // Hues: Blue/Purple/Pink. Denser = shifting towards Pink/White? 563 - // Stable random hue for this nebula based on position 564 - // Use position as seed-ish 565 - const hueSeed = Math.abs(Math.sin(n.x * n.y * n.z)); 566 - const hue = 200 + hueSeed * 80; 567 - 568 - // Radius: Denser areas get BIGGER nebulae to cover the cluster 569 - const radius = (600 + intensity * 800) * scale; 570 - 571 - // Opacity: Denser = more opaque 572 - const alpha = 0.2 + intensity * 0.5; // 0.2 to 0.7 573 - 574 - // Create gradient 575 - const circle = new Konva.Circle({ 553 + const rect = new Konva.Rect({ 576 554 x: screenX, 577 555 y: screenY, 578 - radius: radius, 579 - fillRadialGradientStartPoint: { x: 0, y: 0 }, 580 - fillRadialGradientStartRadius: 0, 581 - fillRadialGradientEndPoint: { x: 0, y: 0 }, 582 - fillRadialGradientEndRadius: radius, 583 - fillRadialGradientColorStops: [ 584 - 0, `hsla(${hue}, 90%, 60%, ${alpha})`, 585 - 1, 'hsla(0, 0%, 0%, 0)' 586 - ], 587 - opacity: 1, 556 + width: size, 557 + height: size, 558 + fill: d.color, 559 + opacity: d.alpha, 588 560 }); 589 - layer.add(circle); 561 + layer.add(rect); 590 562 } 591 563 } 592 564 593 - // 0.5. Void Noise / Space Dust 594 - for (const d of dust) { 595 - const { x: rotX, y: rotY, z: rotZ } = rotatePoint(d.x, d.y, d.z); 565 + layer.draw(); 566 + 567 + const sharpImg = await (stage.toCanvas() as unknown as Canvas).toSharp(); 568 + const buffer = await sharpImg.webp({ effort: 6, quality: 30, smartDeblock: true }).toBuffer(); 569 + await writeFile(DUST_FILE, buffer); 570 + 571 + const projected: Record<string, ProjectedTrans> = {}; 572 + 573 + const fmt = (n: number) => n.toFixed(2); // Round to 2 decimal places 574 + 575 + // 0. Universe Noise / Heatmap (Background Nebulae) 576 + let nebulaIndex = 0; 577 + for (const n of nebulae) { 578 + // Rotate matches star rotation 579 + const { x: rotX, y: rotY, z: rotZ } = rotatePoint(n.x, n.y, n.z); 596 580 581 + // Render if in front of camera 597 582 if (rotZ > 100) { 598 583 const scale = fov / rotZ; 599 584 const screenX = cx + rotX * scale; 600 585 const screenY = cy + (rotY * scale * -1); 601 586 602 - const size = d.sizeFactor * scale; 587 + // Density -> Size & Opacity 588 + const intensity = Math.min(1, n.density / 25); 603 589 604 - const rect = new Konva.Rect({ 605 - x: screenX, 606 - y: screenY, 607 - width: size, 608 - height: size, 609 - fill: d.color, 610 - opacity: d.alpha, 611 - }); 612 - layer.add(rect); 590 + const hueSeed = Math.abs(Math.sin(n.x * n.y * n.z)); 591 + const hue = 200 + hueSeed * 80; 592 + 593 + const radius = (600 + intensity * 800) * scale; 594 + const alpha = 0.2 + intensity * 0.5; 595 + 596 + const gradId = `nebula-${nebulaIndex++}`; 597 + defsContent += ` 598 + <radialGradient id="${gradId}" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"> 599 + <stop offset="0%" stop-color="hsla(${fmt(hue)}, 90%, 60%, ${fmt(alpha)})" /> 600 + <stop offset="100%" stop-color="hsla(0, 0%, 0%, 0)" /> 601 + </radialGradient>`; 602 + 603 + svgBody += `<circle cx="${fmt(screenX)}" cy="${fmt(screenY)}" r="${fmt(radius)}" fill="url(#${gradId})" opacity="1" />`; 613 604 } 614 605 } 615 606 616 607 // 1. Projection pass 617 608 for (const star of stars) { 618 - // Rotate 619 609 const { x: rotX, y: rotY, z: rotZ } = rotatePoint(star.x, star.y, star.z); 620 610 621 611 if (rotZ > 10) { 622 612 const scale = fov / rotZ; 623 613 const screenX = cx + rotX * scale; 624 - const screenY = cy + (rotY * scale * -1); // Flip Y for screen coords 614 + const screenY = cy + (rotY * scale * -1); 625 615 626 616 projected[star.domain] = { x: screenX, y: screenY, scale, z: rotZ }; 627 617 } 628 618 } 629 619 630 - // 2. Draw connections (lines) first so they are behind stars 631 - // Track drawn connections to avoid duplicates (A-B and B-A) 620 + // 2. Draw connections 632 621 const drawnConnections = new Set<string>(); 633 622 634 - // Need quick lookup for stars now that we don't have the map handy (or we rebuild it) 635 - const starMap = new Map<string, Star>(); 636 - stars.forEach(s => starMap.set(s.domain, s)); 637 - 638 623 type RenderLine = { 639 624 p1: { x: number, y: number, z: number }; 640 625 p2: { x: number, y: number, z: number }; ··· 650 635 651 636 if (star.visualConnections) { 652 637 for (const target of star.visualConnections) { 653 - // Unique key for connection 654 638 const key = [star.domain, target].sort().join('-'); 655 639 if (drawnConnections.has(key)) continue; 656 640 drawnConnections.add(key); ··· 664 648 } 665 649 } 666 650 667 - // Sort lines by depth (furthest first) for correct layering 668 - // Higher Z = further away 669 651 linesToDraw.sort((a, b) => b.avgZ - a.avgZ); 670 652 671 653 for (const line of linesToDraw) { 672 654 const { p1, p2, avgZ } = line; 673 655 674 - // distance fading 675 - // Closer = more opaque. Max opacity 0.8 at z=0, drops to 0 at z=3000 676 656 const opacity = Math.max(0.4, Math.min(1, 1 - (avgZ / 3000))); 677 657 const strokeWidth = Math.max(0.2 * RESOLUTION_SCALE, 1.5 * RESOLUTION_SCALE * (1000 / avgZ)); 678 658 679 - // Halo (Occlusion) - Draw a thick black line behind the white line 680 - // Only effective if there are things behind it (which sorted order ensures) 681 - const halo = new Konva.Line({ 682 - points: [p1.x, p1.y, p2.x, p2.y], 683 - stroke: '#000000', 684 - strokeWidth: strokeWidth + strokeWidth * opacity, // 2px padding on each side 685 - opacity: 1, // Full occlusion 686 - tension: 0, 687 - }); 688 - layer.add(halo); 659 + // Halo (black line behind) 660 + // 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" />`; 689 661 690 662 // Actual Line 691 - const l = new Konva.Line({ 692 - points: [p1.x, p1.y, p2.x, p2.y], 693 - stroke: '#FFFFFF', 694 - strokeWidth, 695 - opacity, 696 - tension: 0, 697 - }); 698 - layer.add(l); 663 + 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" />`; 699 664 } 700 665 701 - const drawnStars: { star: Star, radius: number, opacity: number, proj: ProjectedTrans }[] = []; 702 - // 3. draw star halos 666 + // 3. Draw Stars 703 667 for (const star of stars) { 704 668 if (!projected[star.domain]) continue; 705 669 const p = projected[star.domain]; ··· 708 672 const importance = Math.min(1.5, 1 + connectionCount * 0.1); 709 673 710 674 const radius = Math.max(1 * RESOLUTION_SCALE, 25 * p.scale * importance) * 0.4; 711 - const haloRadius = radius * 1.6; 712 - const opacity = Math.min(1, Math.max(0.2, 1000 / p.z)); 713 - const haloOpacity = opacity * 0.4; 675 + const haloRadius = radius * 1.85; 714 676 715 - const rect = new Konva.Rect({ 716 - x: p.x - haloRadius / 2, 717 - y: p.y - haloRadius / 2, 718 - width: haloRadius, 719 - height: haloRadius, 720 - fill: '#FFFFFF', 721 - opacity: haloOpacity, 722 - }); 677 + const strokeWidth = haloRadius - radius; 723 678 724 - layer.add(rect); 725 - drawnStars.push({ star, radius, opacity, proj: p }); 726 - } 679 + const opacity = Math.min(1, Math.max(0.2, 1000 / p.z)); 680 + const haloOpacity = opacity * 0.3; 727 681 728 - // 4. Draw actual stars 729 - for (const { star, radius, opacity, proj } of drawnStars) { 730 - const rect = new Konva.Rect({ 731 - x: proj.x - radius / 2, 732 - y: proj.y - radius / 2, 733 - width: radius, 734 - height: radius, 735 - fill: '#EEEEEE', 736 - opacity, 737 - }); 738 - 739 - layer.add(rect); 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" />`; 740 683 } 741 684 742 - layer.draw(); 685 + // Construct final SVG 686 + const finalSvg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg"> 687 + <defs>${defsContent}</defs> 688 + ${svgBody} 689 + </svg>`; 743 690 744 - const buffer = await (stage.toCanvas() as any).toBuffer('png'); 745 - writeFileSync(OUTPUT_FILE, buffer); 691 + await writeFile(OUTPUT_FILE, finalSvg); 746 692 747 693 // Export projected coordinates for frontend interactivity 748 694 const visibleStars = stars 749 695 .filter(star => projected[star.domain]) 750 696 .map(star => { 751 697 const p = projected[star.domain]; 752 - // Match render logic for radius approx 753 698 const connectionCount = star.connections ? star.connections.length : 0; 754 699 const importance = Math.min(1.5, 1 + connectionCount * 0.1); 755 700 return { ··· 760 705 }; 761 706 }); 762 707 763 - writeFileSync(STARS_FILE, JSON.stringify({ 708 + await writeFile(STARS_FILE, JSON.stringify({ 764 709 width, 765 710 height, 766 711 stars: visibleStars,
+8
eunomia/src/routes/(site)/+layout.svelte
··· 55 55 if (eyePositions === null) { 56 56 eyePositions = data.eyePositions; 57 57 } 58 + 59 + let bgTimestamp = $derived(new Date(data.stars?.meta?.timestamp || 0).getTime()); 58 60 </script> 59 61 60 62 <svelte:head> ··· 64 66 <meta property="og:image" content="https://gaze.systems/icons/gaze_website.webp" /> 65 67 </svelte:head> 66 68 69 + <img 70 + src="/_api/background/dust?t={bgTimestamp}" 71 + alt="" 72 + class="fixed -z-20 w-full h-full object-cover top-0 left-0 pointer-events-none" 73 + /> 67 74 <div 68 75 class=" 69 76 app-grid-background 70 77 fixed -z-10 w-full [height:100%] top-0 left-0 71 78 " 79 + style="background-image: url('/_api/background?t={bgTimestamp}');" 72 80 ></div> 73 81 74 82 <ConstellationOverlay stars={data.stars} {isUIHidden} />
+42 -13
eunomia/src/routes/_api/background/+server.ts
··· 1 - import { readFileSync } from 'node:fs'; 1 + import { readFile, stat } from 'node:fs/promises'; 2 2 import { join } from 'node:path'; 3 + import { brotliCompress, gzip } from 'node:zlib'; 4 + import { env } from '$env/dynamic/private'; 5 + import { initConstellation } from '$lib/constellation'; 3 6 4 - export const GET = async () => { 5 - const DATA_DIR = 'data/constellation'; 6 - const OUTPUT_FILE = join(DATA_DIR, 'background.png'); 7 + export const GET = async ({ request }: { request: Request }) => { 8 + const filePath = join(env.WEBSITE_DATA_DIR, 'constellation', 'background.svg'); 7 9 8 10 try { 9 - const file = readFileSync(OUTPUT_FILE); 10 - return new Response(file, { 11 - headers: { 12 - 'Content-Type': 'image/png', 13 - 'Cache-Control': 'public, max-age=60' // match rotation interval 14 - } 15 - }); 11 + await stat(filePath); 16 12 } catch (e) { 17 - console.error('Error serving background:', e); 18 - return new Response('Not found', { status: 404 }); 13 + await initConstellation(); 14 + } 15 + 16 + try { 17 + const file = await readFile(filePath); 18 + const acceptEncoding = request.headers.get('accept-encoding') || ''; 19 + 20 + let content: any = file; 21 + let contentEncoding: string | null = null; 22 + 23 + if (acceptEncoding.includes('br')) { 24 + content = await new Promise((resolve, reject) => 25 + brotliCompress(file, (err, buf) => (err ? reject(err) : resolve(buf))) 26 + ); 27 + contentEncoding = 'br'; 28 + } else if (acceptEncoding.includes('gzip')) { 29 + content = await new Promise((resolve, reject) => 30 + gzip(file, (err, buf) => (err ? reject(err) : resolve(buf))) 31 + ); 32 + contentEncoding = 'gzip'; 33 + } 34 + 35 + const headers: Record<string, string> = { 36 + 'Content-Type': 'image/svg+xml', 37 + 'Cache-Control': 'public, max-age=31536000, immutable' 38 + }; 39 + 40 + if (contentEncoding) { 41 + headers['Content-Encoding'] = contentEncoding; 42 + } 43 + 44 + return new Response(content, { headers }); 45 + } catch (err) { 46 + console.error(`error serving background: ${err}`); 47 + return new Response('not found', { status: 404 }); 19 48 } 20 49 };
+27
eunomia/src/routes/_api/background/dust/+server.ts
··· 1 + import { readFile, stat } from 'node:fs/promises'; 2 + import { join } from 'node:path'; 3 + import { env } from '$env/dynamic/private'; 4 + import { initConstellation } from '$lib/constellation'; 5 + 6 + export const GET = async () => { 7 + const filePath = join(env.WEBSITE_DATA_DIR, 'constellation', 'background_dust.webp'); 8 + 9 + try { 10 + await stat(filePath); 11 + } catch (e) { 12 + await initConstellation(); 13 + } 14 + 15 + try { 16 + const file = await readFile(filePath); 17 + return new Response(file, { 18 + headers: { 19 + 'Content-Type': 'image/webp', 20 + 'Cache-Control': 'public, max-age=31536000, immutable' 21 + } 22 + }); 23 + } catch (err) { 24 + console.error(`error serving background dust: ${err}`); 25 + return new Response('not found', { status: 404 }); 26 + } 27 + };