Universe / static /app.js
KrishnaGarg's picture
Deploy Asterism Relay vertical slice
2f66c90 verified
Raw
History Blame Contribute Delete
12.7 kB
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js";
const canvas = document.querySelector("#universe");
const hud = {
sector: document.querySelector("#sector"),
status: document.querySelector("#status"),
name: document.querySelector("#body-name"),
phenomenon: document.querySelector("#phenomenon"),
science: document.querySelector("#science"),
transmission: document.querySelector("#transmission"),
};
const engage = document.querySelector("#engage");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020408, 0.00035);
const camera = new THREE.PerspectiveCamera(72, window.innerWidth / window.innerHeight, 0.1, 6000);
camera.position.set(0, 12, 90);
const clock = new THREE.Clock();
const keys = new Set();
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
const yaw = new THREE.Object3D();
const pitch = new THREE.Object3D();
yaw.add(pitch);
pitch.add(camera);
scene.add(yaw);
yaw.position.copy(camera.position);
camera.position.set(0, 0, 0);
let sectorPayload = null;
let payloadRequested = false;
let nearBodyShown = false;
const targetPosition = new THREE.Vector3(0, 20, -450);
const sectorKey = "0:0:-1";
const positionKey = `${targetPosition.x},${targetPosition.y},${targetPosition.z}`;
scene.add(new THREE.AmbientLight(0x8fb6ff, 0.28));
const keyLight = new THREE.PointLight(0xb7f4ff, 850, 1200);
keyLight.position.set(90, 120, 150);
scene.add(keyLight);
const bodyGroup = new THREE.Group();
bodyGroup.position.copy(targetPosition);
scene.add(bodyGroup);
const starMaterial = new THREE.PointsMaterial({
size: 2.4,
sizeAttenuation: true,
transparent: true,
opacity: 0.92,
vertexColors: true,
depthWrite: false,
});
createFallbackStarfield();
loadHygStars();
createBody({
primary: "#8fd7ff",
secondary: "#f7fbff",
emissive: 1.3,
beam: true,
seed: 240519,
noise_scale: 2.7,
atmosphere: 0.45,
});
createNebulae();
requestSectorAhead();
engage.addEventListener("click", () => {
canvas.requestPointerLock?.();
hud.status.textContent = "FLIGHT ONLINE";
engage.classList.add("hidden");
});
document.addEventListener("pointerlockchange", () => {
if (document.pointerLockElement !== canvas) engage.classList.remove("hidden");
});
document.addEventListener("mousemove", (event) => {
if (document.pointerLockElement !== canvas) return;
yaw.rotation.y -= event.movementX * 0.0022;
pitch.rotation.x -= event.movementY * 0.0022;
pitch.rotation.x = THREE.MathUtils.clamp(pitch.rotation.x, -1.4, 1.4);
});
document.addEventListener("keydown", (event) => keys.add(event.code));
document.addEventListener("keyup", (event) => keys.delete(event.code));
window.addEventListener("resize", onResize);
animate();
async function requestSectorAhead() {
if (payloadRequested) return;
payloadRequested = true;
hud.status.textContent = "AUTHORING SECTOR";
try {
const response = await fetch(`/api/sector/${encodeURIComponent(sectorKey)}?position=${encodeURIComponent(positionKey)}`);
if (!response.ok) throw new Error(`Sector request failed: ${response.status}`);
sectorPayload = await response.json();
} catch (error) {
hud.status.textContent = "CACHE ERROR";
hud.science.textContent = "The sector author did not respond. Reload the page and try again.";
return;
}
applySectorPayload(sectorPayload);
hud.status.textContent = "CACHE READY";
}
function applySectorPayload(payload) {
hud.sector.textContent = `SECTOR ${payload.sector_key}`;
hud.name.textContent = payload.body.name;
hud.phenomenon.textContent = payload.fact_layer.title;
hud.science.textContent = "";
hud.transmission.textContent = "";
createBody(payload.shader);
}
function revealPayload() {
if (!sectorPayload || nearBodyShown) return;
nearBodyShown = true;
hud.science.textContent = sectorPayload.fact_layer.explanation;
hud.transmission.textContent = `“${sectorPayload.fiction_layer.transmission}”`;
hud.status.textContent = "TRANSMISSION LOCK";
}
function createBody(shader) {
bodyGroup.clear();
const rng = seeded(shader.seed);
const geometry = new THREE.SphereGeometry(34, 96, 64);
const uniforms = {
time: { value: 0 },
primary: { value: new THREE.Color(shader.primary) },
secondary: { value: new THREE.Color(shader.secondary) },
noiseScale: { value: shader.noise_scale },
emissive: { value: shader.emissive },
};
const material = new THREE.ShaderMaterial({
uniforms,
vertexShader: `
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vNormal = normalize(normalMatrix * normal);
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform vec3 primary;
uniform vec3 secondary;
uniform float noiseScale;
uniform float emissive;
varying vec3 vNormal;
varying vec3 vPosition;
float hash(vec3 p) {
return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float n = mix(
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),
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),
f.z
);
return n;
}
void main() {
vec3 p = normalize(vPosition) * noiseScale + vec3(time * 0.06, 0.0, time * 0.03);
float bands = smoothstep(0.25, 0.9, noise(p) + 0.25 * sin(p.y * 7.0 + time));
float fresnel = pow(1.0 - max(dot(vNormal, vec3(0.0, 0.0, 1.0)), 0.0), 2.5);
vec3 color = mix(primary, secondary, bands) * (0.75 + emissive * 0.3) + fresnel * secondary;
gl_FragColor = vec4(color, 1.0);
}
`,
});
const sphere = new THREE.Mesh(geometry, material);
sphere.userData.uniforms = uniforms;
bodyGroup.add(sphere);
const atmosphere = new THREE.Mesh(
new THREE.SphereGeometry(38 + shader.atmosphere * 12, 64, 48),
new THREE.MeshBasicMaterial({
color: new THREE.Color(shader.secondary),
transparent: true,
opacity: 0.16 + shader.atmosphere * 0.12,
blending: THREE.AdditiveBlending,
depthWrite: false,
}),
);
bodyGroup.add(atmosphere);
if (shader.beam) {
const beamMat = new THREE.MeshBasicMaterial({
color: new THREE.Color(shader.primary),
transparent: true,
opacity: 0.34,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const beamGeo = new THREE.CylinderGeometry(2.2, 10, 360, 32, 1, true);
const beamA = new THREE.Mesh(beamGeo, beamMat);
beamA.rotation.z = Math.PI / 2;
const beamB = beamA.clone();
beamB.rotation.z = -Math.PI / 2;
bodyGroup.add(beamA, beamB);
}
bodyGroup.rotation.set(rng() * Math.PI, rng() * Math.PI, 0);
}
function createFallbackStarfield() {
const positions = [];
const colors = [];
const rng = seeded(1107);
for (let i = 0; i < 1600; i++) {
const radius = 1500 + rng() * 3800;
const theta = rng() * Math.PI * 2;
const phi = Math.acos(rng() * 2 - 1);
positions.push(
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi),
radius * Math.sin(phi) * Math.sin(theta),
);
const c = new THREE.Color().setHSL(0.56 + rng() * 0.12, 0.45, 0.72 + rng() * 0.22);
colors.push(c.r, c.g, c.b);
}
setStarfield(positions, colors);
}
async function loadHygStars() {
const response = await fetch("/data/hyg_sample.csv").catch(() => null);
if (!response?.ok) return;
const text = await response.text();
const rows = text.trim().split(/\r?\n/).slice(1);
const positions = [];
const colors = [];
for (const row of rows) {
const [id, proper, ra, dec, dist, mag, spect] = row.split(",");
const pos = raDecToVector(Number(ra), Number(dec), Math.min(5200, Number(dist) * 18 + 900));
positions.push(pos.x, pos.y, pos.z);
const color = spectralColor(spect, Number(mag));
colors.push(color.r, color.g, color.b);
}
if (positions.length) setStarfield(positions, colors, 9.0);
}
function setStarfield(positions, colors, size = 2.4) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
starMaterial.size = size;
const points = new THREE.Points(geometry, starMaterial);
const old = scene.getObjectByName("starfield");
if (old) scene.remove(old);
points.name = "starfield";
scene.add(points);
}
function createNebulae() {
const texture = makeNebulaTexture();
const material = new THREE.SpriteMaterial({
map: texture,
color: 0x78ffe0,
transparent: true,
opacity: 0.22,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const positions = [
[-420, 150, -900, 520],
[560, -120, -1250, 720],
[-100, 420, -1700, 900],
];
for (const [x, y, z, scale] of positions) {
const sprite = new THREE.Sprite(material.clone());
sprite.position.set(x, y, z);
sprite.scale.set(scale, scale * 0.58, 1);
scene.add(sprite);
}
}
function makeNebulaTexture() {
const nebulaCanvas = document.createElement("canvas");
nebulaCanvas.width = 256;
nebulaCanvas.height = 256;
const ctx = nebulaCanvas.getContext("2d");
const gradient = ctx.createRadialGradient(128, 128, 12, 128, 128, 128);
gradient.addColorStop(0, "rgba(255, 255, 255, 0.55)");
gradient.addColorStop(0.35, "rgba(98, 240, 212, 0.22)");
gradient.addColorStop(0.7, "rgba(255, 79, 139, 0.13)");
gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 256, 256);
return new THREE.CanvasTexture(nebulaCanvas);
}
function animate() {
const dt = Math.min(clock.getDelta(), 0.05);
updateFlight(dt);
const t = clock.elapsedTime;
bodyGroup.rotation.y += dt * 0.22;
for (const child of bodyGroup.children) {
if (child.userData.uniforms) child.userData.uniforms.time.value = t;
}
const dist = yaw.position.distanceTo(targetPosition);
if (dist < 650) requestSectorAhead();
if (dist < 175) revealPayload();
if (!nearBodyShown && sectorPayload) hud.status.textContent = `${Math.round(dist)} KM`;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function updateFlight(dt) {
direction.set(0, 0, 0);
if (keys.has("KeyW") || keys.has("ArrowUp")) direction.z -= 1;
if (keys.has("KeyS") || keys.has("ArrowDown")) direction.z += 1;
if (keys.has("KeyA") || keys.has("ArrowLeft")) direction.x -= 1;
if (keys.has("KeyD") || keys.has("ArrowRight")) direction.x += 1;
if (keys.has("Space")) direction.y += 1;
if (keys.has("ShiftLeft") || keys.has("ShiftRight")) direction.y -= 1;
direction.normalize();
const speed = keys.has("ControlLeft") || keys.has("ControlRight") ? 260 : 145;
velocity.lerp(direction.multiplyScalar(speed), 0.12);
yaw.translateX(velocity.x * dt);
yaw.translateY(velocity.y * dt);
yaw.translateZ(velocity.z * dt);
}
function raDecToVector(raHours, decDeg, radius) {
const ra = (raHours / 24) * Math.PI * 2;
const dec = THREE.MathUtils.degToRad(decDeg);
return new THREE.Vector3(
radius * Math.cos(dec) * Math.cos(ra),
radius * Math.sin(dec),
radius * Math.cos(dec) * Math.sin(ra),
);
}
function spectralColor(spect = "G", mag = 1) {
const letter = spect.trim()[0]?.toUpperCase();
const palette = {
O: 0x9bbcff,
B: 0xaabfff,
A: 0xd8e7ff,
F: 0xfff4df,
G: 0xffe7b5,
K: 0xffbd6f,
M: 0xff8866,
};
const color = new THREE.Color(palette[letter] || 0xffffff);
const boost = THREE.MathUtils.clamp(1.4 - (mag + 1.5) * 0.12, 0.45, 1.4);
return color.multiplyScalar(boost);
}
function seeded(seed) {
let state = Number(seed) >>> 0;
return () => {
state += 0x6d2b79f5;
let t = state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}