tangled
alpha
login
or
join now
ptr.pet
/
endpoint
0
fork
atom
data endpoint for entity 90008 (aka. a website)
0
fork
atom
overview
issues
pulls
pipelines
optimize skyline bg image sizes
ptr.pet
2 months ago
8e31eb0d
444cc7fa
verified
This commit was signed with the committer's
known signature
.
ptr.pet
SSH Key Fingerprint:
SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw=
+370
-162
6 changed files
expand all
collapse all
unified
split
deno.lock
eunomia
package.json
src
lib
constellation.ts
routes
(site)
+layout.svelte
_api
background
+server.ts
dust
+server.ts
+198
deno.lock
reviewed
···
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
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
141
+
},
142
142
+
"@emnapi/runtime@1.7.1": {
143
143
+
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
144
144
+
"dependencies": [
145
145
+
"tslib"
146
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
350
+
},
351
351
+
"@img/colour@1.0.0": {
352
352
+
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="
353
353
+
},
354
354
+
"@img/sharp-darwin-arm64@0.34.5": {
355
355
+
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
356
356
+
"optionalDependencies": [
357
357
+
"@img/sharp-libvips-darwin-arm64"
358
358
+
],
359
359
+
"os": ["darwin"],
360
360
+
"cpu": ["arm64"]
361
361
+
},
362
362
+
"@img/sharp-darwin-x64@0.34.5": {
363
363
+
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
364
364
+
"optionalDependencies": [
365
365
+
"@img/sharp-libvips-darwin-x64"
366
366
+
],
367
367
+
"os": ["darwin"],
368
368
+
"cpu": ["x64"]
369
369
+
},
370
370
+
"@img/sharp-libvips-darwin-arm64@1.2.4": {
371
371
+
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
372
372
+
"os": ["darwin"],
373
373
+
"cpu": ["arm64"]
374
374
+
},
375
375
+
"@img/sharp-libvips-darwin-x64@1.2.4": {
376
376
+
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
377
377
+
"os": ["darwin"],
378
378
+
"cpu": ["x64"]
379
379
+
},
380
380
+
"@img/sharp-libvips-linux-arm64@1.2.4": {
381
381
+
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
382
382
+
"os": ["linux"],
383
383
+
"cpu": ["arm64"]
384
384
+
},
385
385
+
"@img/sharp-libvips-linux-arm@1.2.4": {
386
386
+
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
387
387
+
"os": ["linux"],
388
388
+
"cpu": ["arm"]
389
389
+
},
390
390
+
"@img/sharp-libvips-linux-ppc64@1.2.4": {
391
391
+
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
392
392
+
"os": ["linux"],
393
393
+
"cpu": ["ppc64"]
394
394
+
},
395
395
+
"@img/sharp-libvips-linux-riscv64@1.2.4": {
396
396
+
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
397
397
+
"os": ["linux"],
398
398
+
"cpu": ["riscv64"]
399
399
+
},
400
400
+
"@img/sharp-libvips-linux-s390x@1.2.4": {
401
401
+
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
402
402
+
"os": ["linux"],
403
403
+
"cpu": ["s390x"]
404
404
+
},
405
405
+
"@img/sharp-libvips-linux-x64@1.2.4": {
406
406
+
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
407
407
+
"os": ["linux"],
408
408
+
"cpu": ["x64"]
409
409
+
},
410
410
+
"@img/sharp-libvips-linuxmusl-arm64@1.2.4": {
411
411
+
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
412
412
+
"os": ["linux"],
413
413
+
"cpu": ["arm64"]
414
414
+
},
415
415
+
"@img/sharp-libvips-linuxmusl-x64@1.2.4": {
416
416
+
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
417
417
+
"os": ["linux"],
418
418
+
"cpu": ["x64"]
419
419
+
},
420
420
+
"@img/sharp-linux-arm64@0.34.5": {
421
421
+
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
422
422
+
"optionalDependencies": [
423
423
+
"@img/sharp-libvips-linux-arm64"
424
424
+
],
425
425
+
"os": ["linux"],
426
426
+
"cpu": ["arm64"]
427
427
+
},
428
428
+
"@img/sharp-linux-arm@0.34.5": {
429
429
+
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
430
430
+
"optionalDependencies": [
431
431
+
"@img/sharp-libvips-linux-arm"
432
432
+
],
433
433
+
"os": ["linux"],
434
434
+
"cpu": ["arm"]
435
435
+
},
436
436
+
"@img/sharp-linux-ppc64@0.34.5": {
437
437
+
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
438
438
+
"optionalDependencies": [
439
439
+
"@img/sharp-libvips-linux-ppc64"
440
440
+
],
441
441
+
"os": ["linux"],
442
442
+
"cpu": ["ppc64"]
443
443
+
},
444
444
+
"@img/sharp-linux-riscv64@0.34.5": {
445
445
+
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
446
446
+
"optionalDependencies": [
447
447
+
"@img/sharp-libvips-linux-riscv64"
448
448
+
],
449
449
+
"os": ["linux"],
450
450
+
"cpu": ["riscv64"]
451
451
+
},
452
452
+
"@img/sharp-linux-s390x@0.34.5": {
453
453
+
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
454
454
+
"optionalDependencies": [
455
455
+
"@img/sharp-libvips-linux-s390x"
456
456
+
],
457
457
+
"os": ["linux"],
458
458
+
"cpu": ["s390x"]
459
459
+
},
460
460
+
"@img/sharp-linux-x64@0.34.5": {
461
461
+
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
462
462
+
"optionalDependencies": [
463
463
+
"@img/sharp-libvips-linux-x64"
464
464
+
],
465
465
+
"os": ["linux"],
466
466
+
"cpu": ["x64"]
467
467
+
},
468
468
+
"@img/sharp-linuxmusl-arm64@0.34.5": {
469
469
+
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
470
470
+
"optionalDependencies": [
471
471
+
"@img/sharp-libvips-linuxmusl-arm64"
472
472
+
],
473
473
+
"os": ["linux"],
474
474
+
"cpu": ["arm64"]
475
475
+
},
476
476
+
"@img/sharp-linuxmusl-x64@0.34.5": {
477
477
+
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
478
478
+
"optionalDependencies": [
479
479
+
"@img/sharp-libvips-linuxmusl-x64"
480
480
+
],
481
481
+
"os": ["linux"],
482
482
+
"cpu": ["x64"]
483
483
+
},
484
484
+
"@img/sharp-wasm32@0.34.5": {
485
485
+
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
486
486
+
"dependencies": [
487
487
+
"@emnapi/runtime"
488
488
+
],
489
489
+
"cpu": ["wasm32"]
490
490
+
},
491
491
+
"@img/sharp-win32-arm64@0.34.5": {
492
492
+
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
493
493
+
"os": ["win32"],
494
494
+
"cpu": ["arm64"]
495
495
+
},
496
496
+
"@img/sharp-win32-ia32@0.34.5": {
497
497
+
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
498
498
+
"os": ["win32"],
499
499
+
"cpu": ["ia32"]
500
500
+
},
501
501
+
"@img/sharp-win32-x64@0.34.5": {
502
502
+
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
503
503
+
"os": ["win32"],
504
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
2184
+
"sharp@0.34.5": {
2185
2185
+
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
2186
2186
+
"dependencies": [
2187
2187
+
"@img/colour",
2188
2188
+
"detect-libc",
2189
2189
+
"semver"
2190
2190
+
],
2191
2191
+
"optionalDependencies": [
2192
2192
+
"@img/sharp-darwin-arm64",
2193
2193
+
"@img/sharp-darwin-x64",
2194
2194
+
"@img/sharp-libvips-darwin-arm64",
2195
2195
+
"@img/sharp-libvips-darwin-x64",
2196
2196
+
"@img/sharp-libvips-linux-arm",
2197
2197
+
"@img/sharp-libvips-linux-arm64",
2198
2198
+
"@img/sharp-libvips-linux-ppc64",
2199
2199
+
"@img/sharp-libvips-linux-riscv64",
2200
2200
+
"@img/sharp-libvips-linux-s390x",
2201
2201
+
"@img/sharp-libvips-linux-x64",
2202
2202
+
"@img/sharp-libvips-linuxmusl-arm64",
2203
2203
+
"@img/sharp-libvips-linuxmusl-x64",
2204
2204
+
"@img/sharp-linux-arm",
2205
2205
+
"@img/sharp-linux-arm64",
2206
2206
+
"@img/sharp-linux-ppc64",
2207
2207
+
"@img/sharp-linux-riscv64",
2208
2208
+
"@img/sharp-linux-s390x",
2209
2209
+
"@img/sharp-linux-x64",
2210
2210
+
"@img/sharp-linuxmusl-arm64",
2211
2211
+
"@img/sharp-linuxmusl-x64",
2212
2212
+
"@img/sharp-wasm32",
2213
2213
+
"@img/sharp-win32-arm64",
2214
2214
+
"@img/sharp-win32-ia32",
2215
2215
+
"@img/sharp-win32-x64"
2216
2216
+
],
2217
2217
+
"scripts": true
2218
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
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
reviewed
···
47
47
"node-fetch": "^3.3.2",
48
48
"prometheus-remote-write": "^0.5.1",
49
49
"robots-parser": "^3.0.1",
50
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
reviewed
···
1
1
import Konva from 'konva';
2
2
import 'konva/skia-backend';
3
3
-
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
3
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
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
9
-
const OUTPUT_FILE = join(DATA_DIR, 'background.png');
10
10
+
const OUTPUT_FILE = join(DATA_DIR, 'background.svg');
11
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
423
-
// Uniform distribution, no avoidance
424
424
-
const baseAlpha = 0.1 + rng.next() * 0.2;
425
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
442
-
mkdirSync(DATA_DIR, { recursive: true });
443
443
+
try {
444
444
+
await stat(DATA_DIR);
445
445
+
} catch {
446
446
+
await mkdir(DATA_DIR, { recursive: true });
447
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
454
-
// Save legacy map for stars if strictly needed, but better to save the whole object
455
455
-
// The render function needs the whole object now.
456
456
-
// We'll save the structure exactly as ConstellationData
457
457
-
458
458
-
writeFileSync(GRAPH_FILE, JSON.stringify({ stars, nebulae, dust }));
459
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
471
-
if (!existsSync(GRAPH_FILE)) {
472
472
+
try {
473
473
+
await stat(GRAPH_FILE);
474
474
+
} catch {
472
475
await initConstellation();
473
476
return;
474
477
}
475
478
476
479
const start = Date.now();
477
477
-
console.log('rendering constellation...');
480
480
+
console.log('rendering constellation to SVG...');
478
481
479
479
-
const constellationData: ConstellationData = JSON.parse(readFileSync(GRAPH_FILE, 'utf-8'));
482
482
+
const constellationData: ConstellationData = JSON.parse(await readFile(GRAPH_FILE, 'utf-8'));
480
483
const { stars, nebulae, dust } = constellationData;
481
484
482
482
-
// Canvas setup
483
483
-
const RESOLUTION_SCALE = 2; // 4K resolution
485
485
+
const RESOLUTION_SCALE = 1;
484
486
const width = 1920 * RESOLUTION_SCALE;
485
487
const height = 1080 * RESOLUTION_SCALE;
486
488
487
487
-
// Create stage and layer
488
488
-
const stage = new Konva.Stage({
489
489
-
width,
490
490
-
height,
491
491
-
});
492
492
-
493
493
-
const layer = new Konva.Layer({ imageSmoothingEnabled: false });
494
494
-
stage.add(layer);
495
495
-
496
496
-
// Background
497
497
-
const rect = new Konva.Rect({
498
498
-
width,
499
499
-
height,
500
500
-
fill: '#000000',
501
501
-
});
502
502
-
layer.add(rect);
503
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
537
-
// Project and draw
538
538
-
const projected: Record<string, ProjectedTrans> = {};
522
522
+
// Initialize SVG content
523
523
+
let svgBody = '';
524
524
+
let defsContent = '';
539
525
540
540
-
// 0. Universe Noise / Heatmap (Background Nebulae)
541
541
-
// Draw this BEFORE everything else so it's in the background
526
526
+
// 0.5 Render Dust via Konva
527
527
+
// Use a Stage/Layer for dust only, transparent background
528
528
+
const stage = new Konva.Stage({
529
529
+
width,
530
530
+
height,
531
531
+
});
532
532
+
const layer = new Konva.Layer();
533
533
+
stage.add(layer);
542
534
543
543
-
// Algorithm: Find dense clusters of stars to place nebulae
544
544
-
// 1. We'll use a simplified density estimation.
545
545
-
// Pick N random 'probe' points (existing stars) and calculate how many neighbors they have within Radius R.
546
546
-
// Higher neighbor count = higher density = larger/brighter nebula.
535
535
+
const rect = new Konva.Rect({
536
536
+
width,
537
537
+
height,
538
538
+
fill: '#000000',
539
539
+
});
540
540
+
layer.add(rect);
547
541
548
548
-
for (const n of nebulae) {
549
549
-
// Rotate matches star rotation
550
550
-
const { x: rotX, y: rotY, z: rotZ } = rotatePoint(n.x, n.y, n.z);
542
542
+
// Draw dust particles using Konva
543
543
+
for (const d of dust) {
544
544
+
const { x: rotX, y: rotY, z: rotZ } = rotatePoint(d.x, d.y, d.z);
551
545
552
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
558
-
// Density -> Size & Opacity
559
559
-
// Normalize density: typical range 5 to 50?
560
560
-
const intensity = Math.min(1, n.density / 25);
551
551
+
const size = d.sizeFactor * scale;
561
552
562
562
-
// Hues: Blue/Purple/Pink. Denser = shifting towards Pink/White?
563
563
-
// Stable random hue for this nebula based on position
564
564
-
// Use position as seed-ish
565
565
-
const hueSeed = Math.abs(Math.sin(n.x * n.y * n.z));
566
566
-
const hue = 200 + hueSeed * 80;
567
567
-
568
568
-
// Radius: Denser areas get BIGGER nebulae to cover the cluster
569
569
-
const radius = (600 + intensity * 800) * scale;
570
570
-
571
571
-
// Opacity: Denser = more opaque
572
572
-
const alpha = 0.2 + intensity * 0.5; // 0.2 to 0.7
573
573
-
574
574
-
// Create gradient
575
575
-
const circle = new Konva.Circle({
553
553
+
const rect = new Konva.Rect({
576
554
x: screenX,
577
555
y: screenY,
578
578
-
radius: radius,
579
579
-
fillRadialGradientStartPoint: { x: 0, y: 0 },
580
580
-
fillRadialGradientStartRadius: 0,
581
581
-
fillRadialGradientEndPoint: { x: 0, y: 0 },
582
582
-
fillRadialGradientEndRadius: radius,
583
583
-
fillRadialGradientColorStops: [
584
584
-
0, `hsla(${hue}, 90%, 60%, ${alpha})`,
585
585
-
1, 'hsla(0, 0%, 0%, 0)'
586
586
-
],
587
587
-
opacity: 1,
556
556
+
width: size,
557
557
+
height: size,
558
558
+
fill: d.color,
559
559
+
opacity: d.alpha,
588
560
});
589
589
-
layer.add(circle);
561
561
+
layer.add(rect);
590
562
}
591
563
}
592
564
593
593
-
// 0.5. Void Noise / Space Dust
594
594
-
for (const d of dust) {
595
595
-
const { x: rotX, y: rotY, z: rotZ } = rotatePoint(d.x, d.y, d.z);
565
565
+
layer.draw();
566
566
+
567
567
+
const sharpImg = await (stage.toCanvas() as unknown as Canvas).toSharp();
568
568
+
const buffer = await sharpImg.webp({ effort: 6, quality: 30, smartDeblock: true }).toBuffer();
569
569
+
await writeFile(DUST_FILE, buffer);
570
570
+
571
571
+
const projected: Record<string, ProjectedTrans> = {};
572
572
+
573
573
+
const fmt = (n: number) => n.toFixed(2); // Round to 2 decimal places
574
574
+
575
575
+
// 0. Universe Noise / Heatmap (Background Nebulae)
576
576
+
let nebulaIndex = 0;
577
577
+
for (const n of nebulae) {
578
578
+
// Rotate matches star rotation
579
579
+
const { x: rotX, y: rotY, z: rotZ } = rotatePoint(n.x, n.y, n.z);
596
580
581
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
602
-
const size = d.sizeFactor * scale;
587
587
+
// Density -> Size & Opacity
588
588
+
const intensity = Math.min(1, n.density / 25);
603
589
604
604
-
const rect = new Konva.Rect({
605
605
-
x: screenX,
606
606
-
y: screenY,
607
607
-
width: size,
608
608
-
height: size,
609
609
-
fill: d.color,
610
610
-
opacity: d.alpha,
611
611
-
});
612
612
-
layer.add(rect);
590
590
+
const hueSeed = Math.abs(Math.sin(n.x * n.y * n.z));
591
591
+
const hue = 200 + hueSeed * 80;
592
592
+
593
593
+
const radius = (600 + intensity * 800) * scale;
594
594
+
const alpha = 0.2 + intensity * 0.5;
595
595
+
596
596
+
const gradId = `nebula-${nebulaIndex++}`;
597
597
+
defsContent += `
598
598
+
<radialGradient id="${gradId}" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5">
599
599
+
<stop offset="0%" stop-color="hsla(${fmt(hue)}, 90%, 60%, ${fmt(alpha)})" />
600
600
+
<stop offset="100%" stop-color="hsla(0, 0%, 0%, 0)" />
601
601
+
</radialGradient>`;
602
602
+
603
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
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
624
-
const screenY = cy + (rotY * scale * -1); // Flip Y for screen coords
614
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
630
-
// 2. Draw connections (lines) first so they are behind stars
631
631
-
// Track drawn connections to avoid duplicates (A-B and B-A)
620
620
+
// 2. Draw connections
632
621
const drawnConnections = new Set<string>();
633
622
634
634
-
// Need quick lookup for stars now that we don't have the map handy (or we rebuild it)
635
635
-
const starMap = new Map<string, Star>();
636
636
-
stars.forEach(s => starMap.set(s.domain, s));
637
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
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
667
-
// Sort lines by depth (furthest first) for correct layering
668
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
674
-
// distance fading
675
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
679
-
// Halo (Occlusion) - Draw a thick black line behind the white line
680
680
-
// Only effective if there are things behind it (which sorted order ensures)
681
681
-
const halo = new Konva.Line({
682
682
-
points: [p1.x, p1.y, p2.x, p2.y],
683
683
-
stroke: '#000000',
684
684
-
strokeWidth: strokeWidth + strokeWidth * opacity, // 2px padding on each side
685
685
-
opacity: 1, // Full occlusion
686
686
-
tension: 0,
687
687
-
});
688
688
-
layer.add(halo);
659
659
+
// Halo (black line behind)
660
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
691
-
const l = new Konva.Line({
692
692
-
points: [p1.x, p1.y, p2.x, p2.y],
693
693
-
stroke: '#FFFFFF',
694
694
-
strokeWidth,
695
695
-
opacity,
696
696
-
tension: 0,
697
697
-
});
698
698
-
layer.add(l);
663
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
701
-
const drawnStars: { star: Star, radius: number, opacity: number, proj: ProjectedTrans }[] = [];
702
702
-
// 3. draw star halos
666
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
711
-
const haloRadius = radius * 1.6;
712
712
-
const opacity = Math.min(1, Math.max(0.2, 1000 / p.z));
713
713
-
const haloOpacity = opacity * 0.4;
675
675
+
const haloRadius = radius * 1.85;
714
676
715
715
-
const rect = new Konva.Rect({
716
716
-
x: p.x - haloRadius / 2,
717
717
-
y: p.y - haloRadius / 2,
718
718
-
width: haloRadius,
719
719
-
height: haloRadius,
720
720
-
fill: '#FFFFFF',
721
721
-
opacity: haloOpacity,
722
722
-
});
677
677
+
const strokeWidth = haloRadius - radius;
723
678
724
724
-
layer.add(rect);
725
725
-
drawnStars.push({ star, radius, opacity, proj: p });
726
726
-
}
679
679
+
const opacity = Math.min(1, Math.max(0.2, 1000 / p.z));
680
680
+
const haloOpacity = opacity * 0.3;
727
681
728
728
-
// 4. Draw actual stars
729
729
-
for (const { star, radius, opacity, proj } of drawnStars) {
730
730
-
const rect = new Konva.Rect({
731
731
-
x: proj.x - radius / 2,
732
732
-
y: proj.y - radius / 2,
733
733
-
width: radius,
734
734
-
height: radius,
735
735
-
fill: '#EEEEEE',
736
736
-
opacity,
737
737
-
});
738
738
-
739
739
-
layer.add(rect);
682
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
742
-
layer.draw();
685
685
+
// Construct final SVG
686
686
+
const finalSvg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
687
687
+
<defs>${defsContent}</defs>
688
688
+
${svgBody}
689
689
+
</svg>`;
743
690
744
744
-
const buffer = await (stage.toCanvas() as any).toBuffer('png');
745
745
-
writeFileSync(OUTPUT_FILE, buffer);
691
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
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
763
-
writeFileSync(STARS_FILE, JSON.stringify({
708
708
+
await writeFile(STARS_FILE, JSON.stringify({
764
709
width,
765
710
height,
766
711
stars: visibleStars,
+8
eunomia/src/routes/(site)/+layout.svelte
reviewed
···
55
55
if (eyePositions === null) {
56
56
eyePositions = data.eyePositions;
57
57
}
58
58
+
59
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
69
+
<img
70
70
+
src="/_api/background/dust?t={bgTimestamp}"
71
71
+
alt=""
72
72
+
class="fixed -z-20 w-full h-full object-cover top-0 left-0 pointer-events-none"
73
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
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
reviewed
···
1
1
-
import { readFileSync } from 'node:fs';
1
1
+
import { readFile, stat } from 'node:fs/promises';
2
2
import { join } from 'node:path';
3
3
+
import { brotliCompress, gzip } from 'node:zlib';
4
4
+
import { env } from '$env/dynamic/private';
5
5
+
import { initConstellation } from '$lib/constellation';
3
6
4
4
-
export const GET = async () => {
5
5
-
const DATA_DIR = 'data/constellation';
6
6
-
const OUTPUT_FILE = join(DATA_DIR, 'background.png');
7
7
+
export const GET = async ({ request }: { request: Request }) => {
8
8
+
const filePath = join(env.WEBSITE_DATA_DIR, 'constellation', 'background.svg');
7
9
8
10
try {
9
9
-
const file = readFileSync(OUTPUT_FILE);
10
10
-
return new Response(file, {
11
11
-
headers: {
12
12
-
'Content-Type': 'image/png',
13
13
-
'Cache-Control': 'public, max-age=60' // match rotation interval
14
14
-
}
15
15
-
});
11
11
+
await stat(filePath);
16
12
} catch (e) {
17
17
-
console.error('Error serving background:', e);
18
18
-
return new Response('Not found', { status: 404 });
13
13
+
await initConstellation();
14
14
+
}
15
15
+
16
16
+
try {
17
17
+
const file = await readFile(filePath);
18
18
+
const acceptEncoding = request.headers.get('accept-encoding') || '';
19
19
+
20
20
+
let content: any = file;
21
21
+
let contentEncoding: string | null = null;
22
22
+
23
23
+
if (acceptEncoding.includes('br')) {
24
24
+
content = await new Promise((resolve, reject) =>
25
25
+
brotliCompress(file, (err, buf) => (err ? reject(err) : resolve(buf)))
26
26
+
);
27
27
+
contentEncoding = 'br';
28
28
+
} else if (acceptEncoding.includes('gzip')) {
29
29
+
content = await new Promise((resolve, reject) =>
30
30
+
gzip(file, (err, buf) => (err ? reject(err) : resolve(buf)))
31
31
+
);
32
32
+
contentEncoding = 'gzip';
33
33
+
}
34
34
+
35
35
+
const headers: Record<string, string> = {
36
36
+
'Content-Type': 'image/svg+xml',
37
37
+
'Cache-Control': 'public, max-age=31536000, immutable'
38
38
+
};
39
39
+
40
40
+
if (contentEncoding) {
41
41
+
headers['Content-Encoding'] = contentEncoding;
42
42
+
}
43
43
+
44
44
+
return new Response(content, { headers });
45
45
+
} catch (err) {
46
46
+
console.error(`error serving background: ${err}`);
47
47
+
return new Response('not found', { status: 404 });
19
48
}
20
49
};
+27
eunomia/src/routes/_api/background/dust/+server.ts
reviewed
···
1
1
+
import { readFile, stat } from 'node:fs/promises';
2
2
+
import { join } from 'node:path';
3
3
+
import { env } from '$env/dynamic/private';
4
4
+
import { initConstellation } from '$lib/constellation';
5
5
+
6
6
+
export const GET = async () => {
7
7
+
const filePath = join(env.WEBSITE_DATA_DIR, 'constellation', 'background_dust.webp');
8
8
+
9
9
+
try {
10
10
+
await stat(filePath);
11
11
+
} catch (e) {
12
12
+
await initConstellation();
13
13
+
}
14
14
+
15
15
+
try {
16
16
+
const file = await readFile(filePath);
17
17
+
return new Response(file, {
18
18
+
headers: {
19
19
+
'Content-Type': 'image/webp',
20
20
+
'Cache-Control': 'public, max-age=31536000, immutable'
21
21
+
}
22
22
+
});
23
23
+
} catch (err) {
24
24
+
console.error(`error serving background dust: ${err}`);
25
25
+
return new Response('not found', { status: 404 });
26
26
+
}
27
27
+
};