KrishnaGarg commited on
Commit
2f66c90
·
verified ·
1 Parent(s): 245a924

Deploy Asterism Relay vertical slice

Browse files
Files changed (8) hide show
  1. README.md +23 -8
  2. app.py +160 -0
  3. data/astro_facts.json +92 -0
  4. data/hyg_sample.csv +17 -0
  5. requirements.txt +3 -0
  6. static/app.js +392 -0
  7. static/index.html +27 -0
  8. static/styles.css +127 -0
README.md CHANGED
@@ -1,14 +1,29 @@
1
  ---
2
- title: Universe
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
- license: apache-2.0
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Asterism Relay
3
+ emoji: 🌌
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: gradio
 
 
7
  app_file: app.py
8
  pinned: false
9
+ tags:
10
+ - build-small-hackathon
11
+ - thousand-token-wood
12
+ - custom-ui
13
+ - gradio-server
14
  ---
15
 
16
+ # Asterism Relay
17
+
18
+ An explorable first-person cosmos for the Build Small Hackathon's Thousand Token Wood track.
19
+
20
+ This first vertical slice proves the core loop:
21
+
22
+ - custom Three.js frontend served from `gr.Server`
23
+ - HYG-style real star sample for the background field
24
+ - one deterministic procedural body seeded from position
25
+ - backend sector JSON generated ahead of arrival
26
+ - fact layer selected from a curated local astrophysics knowledge base
27
+ - fiction layer authored separately as a vanished-civilization transmission
28
+
29
+ The production model is intentionally behind a small adapter so the author model can be selected after the planned bake-off.
app.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import random
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
+ from fastapi.staticfiles import StaticFiles
9
+ from gradio import Server
10
+
11
+ try:
12
+ import spaces
13
+ except ImportError: # Local development outside Hugging Face ZeroGPU.
14
+ class _Spaces:
15
+ @staticmethod
16
+ def GPU(*args: Any, **kwargs: Any):
17
+ if args and callable(args[0]) and not kwargs:
18
+ return args[0]
19
+
20
+ def decorator(fn):
21
+ return fn
22
+
23
+ return decorator
24
+
25
+ spaces = _Spaces()
26
+
27
+
28
+ ROOT = Path(__file__).parent
29
+ DATA = ROOT / "data"
30
+ STATIC = ROOT / "static"
31
+
32
+ app = Server()
33
+ app.mount("/static", StaticFiles(directory=STATIC), name="static")
34
+ app.mount("/data", StaticFiles(directory=DATA), name="data")
35
+
36
+
37
+ @lru_cache(maxsize=1)
38
+ def load_facts() -> list[dict[str, Any]]:
39
+ return json.loads((DATA / "astro_facts.json").read_text(encoding="utf-8"))
40
+
41
+
42
+ @lru_cache(maxsize=128)
43
+ def generate_sector_cached(sector_key: str, position_key: str) -> dict[str, Any]:
44
+ facts = load_facts()
45
+ rng = random.Random(f"{sector_key}:{position_key}:asterism-relay")
46
+ concept = rng.choice(facts)
47
+ serial = rng.randrange(100, 999)
48
+ fragment_no = rng.randrange(2, 8)
49
+ hue_shift = rng.random()
50
+
51
+ selected_facts = rng.sample(concept["facts"], k=min(3, len(concept["facts"])))
52
+ explanation = " ".join([concept["one_line"], *selected_facts])
53
+
54
+ names = [
55
+ "Lacuna",
56
+ "Vesper",
57
+ "Aster",
58
+ "Noctil",
59
+ "Mirrorglass",
60
+ "Far Choir",
61
+ "Horizon Loom",
62
+ ]
63
+ civilization_terms = [
64
+ "the Archivists",
65
+ "the Choir of Measures",
66
+ "the Lantern Kin",
67
+ "the Last Cartographers",
68
+ ]
69
+
70
+ return {
71
+ "sector_key": sector_key,
72
+ "body": {
73
+ "id": f"{concept['id']}-{serial}",
74
+ "name": f"{rng.choice(names)} {serial}",
75
+ "type": concept["body_type"],
76
+ "phenomenon": concept["name"],
77
+ "position": parse_position(position_key),
78
+ },
79
+ "fact_layer": {
80
+ "source": "curated_local_knowledge_base",
81
+ "concept_id": concept["id"],
82
+ "title": concept["name"],
83
+ "explanation": explanation,
84
+ "facts": selected_facts,
85
+ },
86
+ "fiction_layer": {
87
+ "civilization": rng.choice(civilization_terms),
88
+ "fragment_id": f"transmission-{fragment_no:02d}",
89
+ "transmission": build_transmission(concept["id"], rng),
90
+ },
91
+ "shader": {
92
+ "seed": rng.randrange(1_000_000, 9_999_999),
93
+ "primary": concept["visual_cues"]["primary_color"],
94
+ "secondary": concept["visual_cues"]["secondary_color"],
95
+ "emissive": concept["visual_cues"]["emissive"],
96
+ "beam": concept["visual_cues"]["beam"],
97
+ "noise_scale": round(1.8 + hue_shift * 3.4, 3),
98
+ "atmosphere": round(0.25 + rng.random() * 0.5, 3),
99
+ },
100
+ }
101
+
102
+
103
+ def parse_position(position_key: str) -> dict[str, float]:
104
+ try:
105
+ x, y, z = [float(part) for part in position_key.split(",")]
106
+ except ValueError:
107
+ x, y, z = 0.0, 20.0, -450.0
108
+ return {"x": x, "y": y, "z": z}
109
+
110
+
111
+ def build_transmission(concept_id: str, rng: random.Random) -> str:
112
+ fragments = {
113
+ "pulsar": [
114
+ "We counted the beacon until the count began answering back.",
115
+ "Their clock survived the cities; follow the pulse, not the dust.",
116
+ ],
117
+ "red_giant": [
118
+ "The old suns taught us expansion is not escape, only a slower goodbye.",
119
+ "When the red light filled our windows, every archive learned to whisper.",
120
+ ],
121
+ "white_dwarf": [
122
+ "A small ember can remember a whole star if the dark is patient enough.",
123
+ "We hid the index in the cooling remnant, where haste cannot follow.",
124
+ ],
125
+ "accretion_disk": [
126
+ "The dark center is silent. The falling light around it confesses everything.",
127
+ "Do not cross the bright ring. Read it from the edge where time still behaves.",
128
+ ],
129
+ "emission_nebula": [
130
+ "New stars were born in the wound, and we mistook their first light for forgiveness.",
131
+ "The cloud glows because something young is demanding to be seen.",
132
+ ],
133
+ }
134
+ return rng.choice(fragments.get(concept_id, fragments["pulsar"]))
135
+
136
+
137
+ @spaces.GPU(duration=20)
138
+ def author_sector_payload(sector_key: str, position_key: str) -> dict[str, Any]:
139
+ # The model adapter belongs here. For the slice, physics text is assembled only
140
+ # from curated facts while the fiction layer remains free to vary by seed.
141
+ return generate_sector_cached(sector_key, position_key)
142
+
143
+
144
+ @app.api(name="generate_sector", concurrency_limit=1)
145
+ def generate_sector(sector_key: str, position_key: str) -> dict[str, Any]:
146
+ return author_sector_payload(sector_key, position_key)
147
+
148
+
149
+ @app.get("/api/sector/{sector_key}")
150
+ async def sector_rest(sector_key: str, position: str = "0,20,-450"):
151
+ return JSONResponse(generate_sector_cached(sector_key, position))
152
+
153
+
154
+ @app.get("/", response_class=HTMLResponse)
155
+ async def homepage():
156
+ return HTMLResponse((STATIC / "index.html").read_text(encoding="utf-8"))
157
+
158
+
159
+ if __name__ == "__main__":
160
+ app.launch(server_name="0.0.0.0", server_port=7860)
data/astro_facts.json ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "pulsar",
4
+ "name": "Pulsar",
5
+ "body_type": "neutron_star",
6
+ "one_line": "A pulsar is a rapidly rotating neutron star whose magnetic poles sweep beams of radiation across space.",
7
+ "facts": [
8
+ "Neutron stars are the collapsed cores left behind by some massive stars after supernova explosions.",
9
+ "A pulsar's radio, X-ray, or gamma-ray pulses are seen when its beam crosses Earth's line of sight.",
10
+ "Pulsars can rotate from a few times per second to hundreds of times per second.",
11
+ "The pulses are extremely regular, which makes some pulsars useful as precise cosmic clocks."
12
+ ],
13
+ "visual_cues": {
14
+ "primary_color": "#8fd7ff",
15
+ "secondary_color": "#f7fbff",
16
+ "emissive": 1.6,
17
+ "beam": true
18
+ }
19
+ },
20
+ {
21
+ "id": "red_giant",
22
+ "name": "Red Giant",
23
+ "body_type": "star",
24
+ "one_line": "A red giant is an aging star that has expanded after exhausting hydrogen fuel in its core.",
25
+ "facts": [
26
+ "As the core contracts, hydrogen fusion continues in a shell around it and the outer layers swell.",
27
+ "Red giants are cooler at their surfaces than Sun-like main-sequence stars, so they appear orange or red.",
28
+ "The Sun is expected to become a red giant late in its life cycle.",
29
+ "Strong stellar winds from red giants can seed nearby space with heavier elements."
30
+ ],
31
+ "visual_cues": {
32
+ "primary_color": "#ff9b5f",
33
+ "secondary_color": "#ffd0a3",
34
+ "emissive": 0.9,
35
+ "beam": false
36
+ }
37
+ },
38
+ {
39
+ "id": "white_dwarf",
40
+ "name": "White Dwarf",
41
+ "body_type": "stellar_remnant",
42
+ "one_line": "A white dwarf is the dense, hot remnant core of a low- or medium-mass star.",
43
+ "facts": [
44
+ "White dwarfs no longer sustain normal fusion in their cores.",
45
+ "A typical white dwarf packs roughly a Sun-like mass into an Earth-sized volume.",
46
+ "They slowly cool over enormous spans of time by radiating stored thermal energy.",
47
+ "In some binary systems, material falling onto a white dwarf can trigger nova outbursts."
48
+ ],
49
+ "visual_cues": {
50
+ "primary_color": "#d7edff",
51
+ "secondary_color": "#ffffff",
52
+ "emissive": 1.2,
53
+ "beam": false
54
+ }
55
+ },
56
+ {
57
+ "id": "accretion_disk",
58
+ "name": "Black Hole Accretion Disk",
59
+ "body_type": "black_hole",
60
+ "one_line": "An accretion disk is a rotating disk of gas heated as it spirals around a compact object such as a black hole.",
61
+ "facts": [
62
+ "Friction and magnetic turbulence in the disk convert orbital energy into heat and radiation.",
63
+ "The black hole itself emits no light, but nearby infalling matter can glow intensely.",
64
+ "Relativistic speeds near a black hole can distort the disk's apparent brightness and shape.",
65
+ "Some accreting black holes launch jets along their rotation axes."
66
+ ],
67
+ "visual_cues": {
68
+ "primary_color": "#ffd166",
69
+ "secondary_color": "#4cc9f0",
70
+ "emissive": 1.8,
71
+ "beam": false
72
+ }
73
+ },
74
+ {
75
+ "id": "emission_nebula",
76
+ "name": "Emission Nebula",
77
+ "body_type": "nebula",
78
+ "one_line": "An emission nebula is a cloud of gas that glows after ultraviolet light from hot stars ionizes it.",
79
+ "facts": [
80
+ "Ionized hydrogen often produces a red glow as electrons recombine with protons.",
81
+ "Many emission nebulae are active star-forming regions.",
82
+ "Dust mixed with the gas can absorb, scatter, and reshape the light we see.",
83
+ "Massive young stars can carve cavities and pillars into surrounding gas clouds."
84
+ ],
85
+ "visual_cues": {
86
+ "primary_color": "#ff4f8b",
87
+ "secondary_color": "#46e0c4",
88
+ "emissive": 0.7,
89
+ "beam": false
90
+ }
91
+ }
92
+ ]
data/hyg_sample.csv ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id,proper,ra,dec,dist,mag,spect
2
+ 1,Sirius,6.7525,-16.7161,2.637,-1.46,A1V
3
+ 2,Canopus,6.3992,-52.6957,94.786,-0.74,A9II
4
+ 3,Alpha Centauri,14.6601,-60.8339,1.338,-0.27,G2V
5
+ 4,Arcturus,14.2610,19.1825,11.257,-0.05,K1.5III
6
+ 5,Vega,18.6156,38.7837,7.678,0.03,A0V
7
+ 6,Capella,5.2782,45.9980,12.927,0.08,G5III
8
+ 7,Rigel,5.2423,-8.2016,264.550,0.13,B8Ia
9
+ 8,Procyon,7.6550,5.2250,3.514,0.34,F5IV
10
+ 9,Betelgeuse,5.9195,7.4071,197.628,0.45,M2Iab
11
+ 10,Altair,19.8464,8.8683,5.129,0.76,A7V
12
+ 11,Aldebaran,4.5987,16.5093,20.433,0.87,K5III
13
+ 12,Spica,13.4199,-11.1613,76.923,0.98,B1III
14
+ 13,Antares,16.4901,-26.4320,169.492,1.06,M1.5Iab
15
+ 14,Pollux,7.7553,28.0262,10.358,1.14,K0III
16
+ 15,Fomalhaut,22.9608,-29.6222,7.704,1.16,A3V
17
+ 16,Deneb,20.6905,45.2803,802.568,1.25,A2Ia
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=6.0.0
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.32.0
static/app.js ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js";
2
+
3
+ const canvas = document.querySelector("#universe");
4
+ const hud = {
5
+ sector: document.querySelector("#sector"),
6
+ status: document.querySelector("#status"),
7
+ name: document.querySelector("#body-name"),
8
+ phenomenon: document.querySelector("#phenomenon"),
9
+ science: document.querySelector("#science"),
10
+ transmission: document.querySelector("#transmission"),
11
+ };
12
+ const engage = document.querySelector("#engage");
13
+
14
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
15
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
16
+ renderer.setSize(window.innerWidth, window.innerHeight);
17
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
18
+
19
+ const scene = new THREE.Scene();
20
+ scene.fog = new THREE.FogExp2(0x020408, 0.00035);
21
+
22
+ const camera = new THREE.PerspectiveCamera(72, window.innerWidth / window.innerHeight, 0.1, 6000);
23
+ camera.position.set(0, 12, 90);
24
+
25
+ const clock = new THREE.Clock();
26
+ const keys = new Set();
27
+ const velocity = new THREE.Vector3();
28
+ const direction = new THREE.Vector3();
29
+ const yaw = new THREE.Object3D();
30
+ const pitch = new THREE.Object3D();
31
+ yaw.add(pitch);
32
+ pitch.add(camera);
33
+ scene.add(yaw);
34
+ yaw.position.copy(camera.position);
35
+ camera.position.set(0, 0, 0);
36
+
37
+ let sectorPayload = null;
38
+ let payloadRequested = false;
39
+ let nearBodyShown = false;
40
+
41
+ const targetPosition = new THREE.Vector3(0, 20, -450);
42
+ const sectorKey = "0:0:-1";
43
+ const positionKey = `${targetPosition.x},${targetPosition.y},${targetPosition.z}`;
44
+
45
+ scene.add(new THREE.AmbientLight(0x8fb6ff, 0.28));
46
+ const keyLight = new THREE.PointLight(0xb7f4ff, 850, 1200);
47
+ keyLight.position.set(90, 120, 150);
48
+ scene.add(keyLight);
49
+
50
+ const bodyGroup = new THREE.Group();
51
+ bodyGroup.position.copy(targetPosition);
52
+ scene.add(bodyGroup);
53
+
54
+ const starMaterial = new THREE.PointsMaterial({
55
+ size: 2.4,
56
+ sizeAttenuation: true,
57
+ transparent: true,
58
+ opacity: 0.92,
59
+ vertexColors: true,
60
+ depthWrite: false,
61
+ });
62
+
63
+ createFallbackStarfield();
64
+ loadHygStars();
65
+ createBody({
66
+ primary: "#8fd7ff",
67
+ secondary: "#f7fbff",
68
+ emissive: 1.3,
69
+ beam: true,
70
+ seed: 240519,
71
+ noise_scale: 2.7,
72
+ atmosphere: 0.45,
73
+ });
74
+ createNebulae();
75
+ requestSectorAhead();
76
+
77
+ engage.addEventListener("click", () => {
78
+ canvas.requestPointerLock?.();
79
+ hud.status.textContent = "FLIGHT ONLINE";
80
+ engage.classList.add("hidden");
81
+ });
82
+
83
+ document.addEventListener("pointerlockchange", () => {
84
+ if (document.pointerLockElement !== canvas) engage.classList.remove("hidden");
85
+ });
86
+
87
+ document.addEventListener("mousemove", (event) => {
88
+ if (document.pointerLockElement !== canvas) return;
89
+ yaw.rotation.y -= event.movementX * 0.0022;
90
+ pitch.rotation.x -= event.movementY * 0.0022;
91
+ pitch.rotation.x = THREE.MathUtils.clamp(pitch.rotation.x, -1.4, 1.4);
92
+ });
93
+
94
+ document.addEventListener("keydown", (event) => keys.add(event.code));
95
+ document.addEventListener("keyup", (event) => keys.delete(event.code));
96
+ window.addEventListener("resize", onResize);
97
+
98
+ animate();
99
+
100
+ async function requestSectorAhead() {
101
+ if (payloadRequested) return;
102
+ payloadRequested = true;
103
+ hud.status.textContent = "AUTHORING SECTOR";
104
+
105
+ try {
106
+ const response = await fetch(`/api/sector/${encodeURIComponent(sectorKey)}?position=${encodeURIComponent(positionKey)}`);
107
+ if (!response.ok) throw new Error(`Sector request failed: ${response.status}`);
108
+ sectorPayload = await response.json();
109
+ } catch (error) {
110
+ hud.status.textContent = "CACHE ERROR";
111
+ hud.science.textContent = "The sector author did not respond. Reload the page and try again.";
112
+ return;
113
+ }
114
+
115
+ applySectorPayload(sectorPayload);
116
+ hud.status.textContent = "CACHE READY";
117
+ }
118
+
119
+ function applySectorPayload(payload) {
120
+ hud.sector.textContent = `SECTOR ${payload.sector_key}`;
121
+ hud.name.textContent = payload.body.name;
122
+ hud.phenomenon.textContent = payload.fact_layer.title;
123
+ hud.science.textContent = "";
124
+ hud.transmission.textContent = "";
125
+ createBody(payload.shader);
126
+ }
127
+
128
+ function revealPayload() {
129
+ if (!sectorPayload || nearBodyShown) return;
130
+ nearBodyShown = true;
131
+ hud.science.textContent = sectorPayload.fact_layer.explanation;
132
+ hud.transmission.textContent = `“${sectorPayload.fiction_layer.transmission}”`;
133
+ hud.status.textContent = "TRANSMISSION LOCK";
134
+ }
135
+
136
+ function createBody(shader) {
137
+ bodyGroup.clear();
138
+ const rng = seeded(shader.seed);
139
+ const geometry = new THREE.SphereGeometry(34, 96, 64);
140
+ const uniforms = {
141
+ time: { value: 0 },
142
+ primary: { value: new THREE.Color(shader.primary) },
143
+ secondary: { value: new THREE.Color(shader.secondary) },
144
+ noiseScale: { value: shader.noise_scale },
145
+ emissive: { value: shader.emissive },
146
+ };
147
+ const material = new THREE.ShaderMaterial({
148
+ uniforms,
149
+ vertexShader: `
150
+ varying vec3 vNormal;
151
+ varying vec3 vPosition;
152
+ void main() {
153
+ vNormal = normalize(normalMatrix * normal);
154
+ vPosition = position;
155
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
156
+ }
157
+ `,
158
+ fragmentShader: `
159
+ uniform float time;
160
+ uniform vec3 primary;
161
+ uniform vec3 secondary;
162
+ uniform float noiseScale;
163
+ uniform float emissive;
164
+ varying vec3 vNormal;
165
+ varying vec3 vPosition;
166
+
167
+ float hash(vec3 p) {
168
+ return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
169
+ }
170
+
171
+ float noise(vec3 p) {
172
+ vec3 i = floor(p);
173
+ vec3 f = fract(p);
174
+ f = f * f * (3.0 - 2.0 * f);
175
+ float n = mix(
176
+ mix(mix(hash(i), hash(i + vec3(1,0,0)), f.x), mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
177
+ mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x), mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y),
178
+ f.z
179
+ );
180
+ return n;
181
+ }
182
+
183
+ void main() {
184
+ vec3 p = normalize(vPosition) * noiseScale + vec3(time * 0.06, 0.0, time * 0.03);
185
+ float bands = smoothstep(0.25, 0.9, noise(p) + 0.25 * sin(p.y * 7.0 + time));
186
+ float fresnel = pow(1.0 - max(dot(vNormal, vec3(0.0, 0.0, 1.0)), 0.0), 2.5);
187
+ vec3 color = mix(primary, secondary, bands) * (0.75 + emissive * 0.3) + fresnel * secondary;
188
+ gl_FragColor = vec4(color, 1.0);
189
+ }
190
+ `,
191
+ });
192
+ const sphere = new THREE.Mesh(geometry, material);
193
+ sphere.userData.uniforms = uniforms;
194
+ bodyGroup.add(sphere);
195
+
196
+ const atmosphere = new THREE.Mesh(
197
+ new THREE.SphereGeometry(38 + shader.atmosphere * 12, 64, 48),
198
+ new THREE.MeshBasicMaterial({
199
+ color: new THREE.Color(shader.secondary),
200
+ transparent: true,
201
+ opacity: 0.16 + shader.atmosphere * 0.12,
202
+ blending: THREE.AdditiveBlending,
203
+ depthWrite: false,
204
+ }),
205
+ );
206
+ bodyGroup.add(atmosphere);
207
+
208
+ if (shader.beam) {
209
+ const beamMat = new THREE.MeshBasicMaterial({
210
+ color: new THREE.Color(shader.primary),
211
+ transparent: true,
212
+ opacity: 0.34,
213
+ blending: THREE.AdditiveBlending,
214
+ depthWrite: false,
215
+ });
216
+ const beamGeo = new THREE.CylinderGeometry(2.2, 10, 360, 32, 1, true);
217
+ const beamA = new THREE.Mesh(beamGeo, beamMat);
218
+ beamA.rotation.z = Math.PI / 2;
219
+ const beamB = beamA.clone();
220
+ beamB.rotation.z = -Math.PI / 2;
221
+ bodyGroup.add(beamA, beamB);
222
+ }
223
+
224
+ bodyGroup.rotation.set(rng() * Math.PI, rng() * Math.PI, 0);
225
+ }
226
+
227
+ function createFallbackStarfield() {
228
+ const positions = [];
229
+ const colors = [];
230
+ const rng = seeded(1107);
231
+ for (let i = 0; i < 1600; i++) {
232
+ const radius = 1500 + rng() * 3800;
233
+ const theta = rng() * Math.PI * 2;
234
+ const phi = Math.acos(rng() * 2 - 1);
235
+ positions.push(
236
+ radius * Math.sin(phi) * Math.cos(theta),
237
+ radius * Math.cos(phi),
238
+ radius * Math.sin(phi) * Math.sin(theta),
239
+ );
240
+ const c = new THREE.Color().setHSL(0.56 + rng() * 0.12, 0.45, 0.72 + rng() * 0.22);
241
+ colors.push(c.r, c.g, c.b);
242
+ }
243
+ setStarfield(positions, colors);
244
+ }
245
+
246
+ async function loadHygStars() {
247
+ const response = await fetch("/data/hyg_sample.csv").catch(() => null);
248
+ if (!response?.ok) return;
249
+ const text = await response.text();
250
+ const rows = text.trim().split(/\r?\n/).slice(1);
251
+ const positions = [];
252
+ const colors = [];
253
+
254
+ for (const row of rows) {
255
+ const [id, proper, ra, dec, dist, mag, spect] = row.split(",");
256
+ const pos = raDecToVector(Number(ra), Number(dec), Math.min(5200, Number(dist) * 18 + 900));
257
+ positions.push(pos.x, pos.y, pos.z);
258
+ const color = spectralColor(spect, Number(mag));
259
+ colors.push(color.r, color.g, color.b);
260
+ }
261
+
262
+ if (positions.length) setStarfield(positions, colors, 9.0);
263
+ }
264
+
265
+ function setStarfield(positions, colors, size = 2.4) {
266
+ const geometry = new THREE.BufferGeometry();
267
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
268
+ geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
269
+ starMaterial.size = size;
270
+ const points = new THREE.Points(geometry, starMaterial);
271
+ const old = scene.getObjectByName("starfield");
272
+ if (old) scene.remove(old);
273
+ points.name = "starfield";
274
+ scene.add(points);
275
+ }
276
+
277
+ function createNebulae() {
278
+ const texture = makeNebulaTexture();
279
+ const material = new THREE.SpriteMaterial({
280
+ map: texture,
281
+ color: 0x78ffe0,
282
+ transparent: true,
283
+ opacity: 0.22,
284
+ blending: THREE.AdditiveBlending,
285
+ depthWrite: false,
286
+ });
287
+ const positions = [
288
+ [-420, 150, -900, 520],
289
+ [560, -120, -1250, 720],
290
+ [-100, 420, -1700, 900],
291
+ ];
292
+ for (const [x, y, z, scale] of positions) {
293
+ const sprite = new THREE.Sprite(material.clone());
294
+ sprite.position.set(x, y, z);
295
+ sprite.scale.set(scale, scale * 0.58, 1);
296
+ scene.add(sprite);
297
+ }
298
+ }
299
+
300
+ function makeNebulaTexture() {
301
+ const nebulaCanvas = document.createElement("canvas");
302
+ nebulaCanvas.width = 256;
303
+ nebulaCanvas.height = 256;
304
+ const ctx = nebulaCanvas.getContext("2d");
305
+ const gradient = ctx.createRadialGradient(128, 128, 12, 128, 128, 128);
306
+ gradient.addColorStop(0, "rgba(255, 255, 255, 0.55)");
307
+ gradient.addColorStop(0.35, "rgba(98, 240, 212, 0.22)");
308
+ gradient.addColorStop(0.7, "rgba(255, 79, 139, 0.13)");
309
+ gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
310
+ ctx.fillStyle = gradient;
311
+ ctx.fillRect(0, 0, 256, 256);
312
+ return new THREE.CanvasTexture(nebulaCanvas);
313
+ }
314
+
315
+ function animate() {
316
+ const dt = Math.min(clock.getDelta(), 0.05);
317
+ updateFlight(dt);
318
+
319
+ const t = clock.elapsedTime;
320
+ bodyGroup.rotation.y += dt * 0.22;
321
+ for (const child of bodyGroup.children) {
322
+ if (child.userData.uniforms) child.userData.uniforms.time.value = t;
323
+ }
324
+
325
+ const dist = yaw.position.distanceTo(targetPosition);
326
+ if (dist < 650) requestSectorAhead();
327
+ if (dist < 175) revealPayload();
328
+ if (!nearBodyShown && sectorPayload) hud.status.textContent = `${Math.round(dist)} KM`;
329
+
330
+ renderer.render(scene, camera);
331
+ requestAnimationFrame(animate);
332
+ }
333
+
334
+ function updateFlight(dt) {
335
+ direction.set(0, 0, 0);
336
+ if (keys.has("KeyW") || keys.has("ArrowUp")) direction.z -= 1;
337
+ if (keys.has("KeyS") || keys.has("ArrowDown")) direction.z += 1;
338
+ if (keys.has("KeyA") || keys.has("ArrowLeft")) direction.x -= 1;
339
+ if (keys.has("KeyD") || keys.has("ArrowRight")) direction.x += 1;
340
+ if (keys.has("Space")) direction.y += 1;
341
+ if (keys.has("ShiftLeft") || keys.has("ShiftRight")) direction.y -= 1;
342
+ direction.normalize();
343
+
344
+ const speed = keys.has("ControlLeft") || keys.has("ControlRight") ? 260 : 145;
345
+ velocity.lerp(direction.multiplyScalar(speed), 0.12);
346
+ yaw.translateX(velocity.x * dt);
347
+ yaw.translateY(velocity.y * dt);
348
+ yaw.translateZ(velocity.z * dt);
349
+ }
350
+
351
+ function raDecToVector(raHours, decDeg, radius) {
352
+ const ra = (raHours / 24) * Math.PI * 2;
353
+ const dec = THREE.MathUtils.degToRad(decDeg);
354
+ return new THREE.Vector3(
355
+ radius * Math.cos(dec) * Math.cos(ra),
356
+ radius * Math.sin(dec),
357
+ radius * Math.cos(dec) * Math.sin(ra),
358
+ );
359
+ }
360
+
361
+ function spectralColor(spect = "G", mag = 1) {
362
+ const letter = spect.trim()[0]?.toUpperCase();
363
+ const palette = {
364
+ O: 0x9bbcff,
365
+ B: 0xaabfff,
366
+ A: 0xd8e7ff,
367
+ F: 0xfff4df,
368
+ G: 0xffe7b5,
369
+ K: 0xffbd6f,
370
+ M: 0xff8866,
371
+ };
372
+ const color = new THREE.Color(palette[letter] || 0xffffff);
373
+ const boost = THREE.MathUtils.clamp(1.4 - (mag + 1.5) * 0.12, 0.45, 1.4);
374
+ return color.multiplyScalar(boost);
375
+ }
376
+
377
+ function seeded(seed) {
378
+ let state = Number(seed) >>> 0;
379
+ return () => {
380
+ state += 0x6d2b79f5;
381
+ let t = state;
382
+ t = Math.imul(t ^ (t >>> 15), t | 1);
383
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
384
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
385
+ };
386
+ }
387
+
388
+ function onResize() {
389
+ camera.aspect = window.innerWidth / window.innerHeight;
390
+ camera.updateProjectionMatrix();
391
+ renderer.setSize(window.innerWidth, window.innerHeight);
392
+ }
static/index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Asterism Relay</title>
7
+ <link rel="stylesheet" href="/static/styles.css" />
8
+ </head>
9
+ <body>
10
+ <main id="app">
11
+ <canvas id="universe"></canvas>
12
+ <section id="hud" aria-live="polite">
13
+ <div class="system-line">
14
+ <span id="sector">SECTOR 0:0:-1</span>
15
+ <span id="status">WARMING CACHE</span>
16
+ </div>
17
+ <h1 id="body-name">Asterism Relay</h1>
18
+ <p id="phenomenon">Acquiring nearby phenomenon</p>
19
+ <p id="science"></p>
20
+ <p id="transmission"></p>
21
+ </section>
22
+ <button id="engage" type="button" aria-label="Engage flight">ENGAGE</button>
23
+ <div id="reticle" aria-hidden="true"></div>
24
+ </main>
25
+ <script type="module" src="/static/app.js?v=2"></script>
26
+ </body>
27
+ </html>
static/styles.css ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family:
4
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
5
+ sans-serif;
6
+ background: #020408;
7
+ color: #f8fbff;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ html,
15
+ body,
16
+ #app {
17
+ width: 100%;
18
+ height: 100%;
19
+ margin: 0;
20
+ overflow: hidden;
21
+ }
22
+
23
+ #universe {
24
+ position: fixed;
25
+ inset: 0;
26
+ width: 100%;
27
+ height: 100%;
28
+ display: block;
29
+ background: #020408;
30
+ }
31
+
32
+ #hud {
33
+ position: fixed;
34
+ left: clamp(16px, 3vw, 40px);
35
+ bottom: clamp(18px, 5vh, 52px);
36
+ width: min(620px, calc(100vw - 32px));
37
+ padding: 0;
38
+ text-shadow: 0 2px 24px rgba(0, 0, 0, 0.78);
39
+ pointer-events: none;
40
+ z-index: 2;
41
+ }
42
+
43
+ .system-line {
44
+ display: flex;
45
+ flex-wrap: wrap;
46
+ gap: 10px 16px;
47
+ align-items: center;
48
+ margin-bottom: 10px;
49
+ font-size: 11px;
50
+ letter-spacing: 0;
51
+ color: #89e9ff;
52
+ }
53
+
54
+ #body-name {
55
+ margin: 0 0 8px;
56
+ font-size: clamp(34px, 6vw, 74px);
57
+ line-height: 0.92;
58
+ letter-spacing: 0;
59
+ font-weight: 760;
60
+ }
61
+
62
+ #phenomenon {
63
+ margin: 0 0 14px;
64
+ color: #fff0b8;
65
+ font-size: clamp(15px, 2vw, 20px);
66
+ }
67
+
68
+ #science,
69
+ #transmission {
70
+ max-width: 58ch;
71
+ margin: 0 0 12px;
72
+ font-size: clamp(14px, 1.6vw, 17px);
73
+ line-height: 1.45;
74
+ }
75
+
76
+ #transmission {
77
+ color: #c5ffed;
78
+ font-style: italic;
79
+ }
80
+
81
+ #engage {
82
+ position: fixed;
83
+ left: 50%;
84
+ top: 50%;
85
+ transform: translate(-50%, -50%);
86
+ min-width: 132px;
87
+ min-height: 44px;
88
+ border: 1px solid rgba(137, 233, 255, 0.6);
89
+ border-radius: 4px;
90
+ background: rgba(2, 10, 18, 0.72);
91
+ color: #f8fbff;
92
+ font-size: 13px;
93
+ font-weight: 700;
94
+ letter-spacing: 0;
95
+ cursor: pointer;
96
+ backdrop-filter: blur(10px);
97
+ z-index: 4;
98
+ }
99
+
100
+ #engage.hidden {
101
+ opacity: 0;
102
+ pointer-events: none;
103
+ }
104
+
105
+ #reticle {
106
+ position: fixed;
107
+ left: 50%;
108
+ top: 50%;
109
+ width: 10px;
110
+ height: 10px;
111
+ border-top: 1px solid rgba(248, 251, 255, 0.72);
112
+ border-left: 1px solid rgba(248, 251, 255, 0.72);
113
+ transform: translate(-50%, -50%) rotate(45deg);
114
+ pointer-events: none;
115
+ z-index: 3;
116
+ }
117
+
118
+ @media (max-width: 640px) {
119
+ #hud {
120
+ bottom: 16px;
121
+ }
122
+
123
+ #science,
124
+ #transmission {
125
+ max-width: 100%;
126
+ }
127
+ }