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