HVRX ORBITAL PLATFORM
3D technical Earth + satellite reference. Load TLEs from Wix Velo endpoint, or use built-in sample set.
MVP — functional demo
Note: Launch planner is a simplified alignment model for MVP (good for “reference feel”). Satellites require a real TLE endpoint (or sample TLEs).
SATELLITES
Search and select. Tracks render for ~90 minutes ahead.
Selected
Lat
Lon
Alt (km)
Speed (km/s)
UTC
STATUSREADY SAT0 SELECTED
// ----------------------------- // Scene / Camera / Renderer // ----------------------------- const canvas = document.getElementById("c"); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.outputColorSpace = THREE.SRGBColorSpace; const scene = new THREE.Scene(); scene.background = new THREE.Color(0x000000); const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 3000); camera.position.set(0, 0, 260); camera.lookAt(0, 0, 0); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.06; controls.minDistance = 140; controls.maxDistance = 700; controls.target.set(0, 0, 0); window.addEventListener("resize", () => { const w = window.innerWidth; const h = window.innerHeight; renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix(); }); // ----------------------------- // Brand colors // ----------------------------- const COLORS = { nebulaViolet: 0x7A5CFF, stellarCyan: 0x00E6FF, solarGold: 0xFFCC33 }; // ----------------------------- // Lighting // ----------------------------- const key = new THREE.DirectionalLight(0xffffff, 1.0); key.position.set(1.2, 0.6, 1.0); scene.add(key); const rim = new THREE.DirectionalLight(0xffffff, 0.6); rim.position.set(-1.2, -0.3, -1.0); scene.add(rim); const fill = new THREE.AmbientLight(0xffffff, 0.28); scene.add(fill); // ----------------------------- // Stars // ----------------------------- function makeStarfield(count = 2000){ const geo = new THREE.BufferGeometry(); const pts = []; for (let i=0; i d * Math.PI / 180; const radToDeg = (r) => r * 180 / Math.PI; function makeCircle(latDeg, color, opacity){ const lat = degToRad(latDeg); const r = Math.cos(lat) * (R + 0.7); const y = Math.sin(lat) * (R + 0.7); const pts = []; const seg = 256; for (let i=0; i<=seg; i++){ const a = (i/seg) * Math.PI * 2; pts.push(new THREE.Vector3(Math.cos(a)*r, y, Math.sin(a)*r)); } const geo = new THREE.BufferGeometry().setFromPoints(pts); const mat = new THREE.LineBasicMaterial({ color, transparent:true, opacity }); return new THREE.Line(geo, mat); } function makeMeridian(lonDeg, color, opacity){ const lon = degToRad(lonDeg); const pts = []; const seg = 256; for (let i=0; i<=seg; i++){ const t = (i/seg) * Math.PI - (Math.PI/2); const x = Math.cos(t) * Math.cos(lon) * (R + 0.7); const y = Math.sin(t) * (R + 0.7); const z = Math.cos(t) * Math.sin(lon) * (R + 0.7); pts.push(new THREE.Vector3(x,y,z)); } const geo = new THREE.BufferGeometry().setFromPoints(pts); const mat = new THREE.LineBasicMaterial({ color, transparent:true, opacity }); return new THREE.Line(geo, mat); } for (let lat=-75; lat<=75; lat+=15){ gridGroup.add(makeCircle(lat, COLORS.stellarCyan, lat===0 ? 0.35 : 0.22)); } for (let lon=0; lon<360; lon+=15){ gridGroup.add(makeMeridian(lon, COLORS.stellarCyan, 0.18)); } const axisMat = new THREE.LineBasicMaterial({ color: COLORS.nebulaViolet, transparent:true, opacity:0.18 }); const axisGeo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, -140, 0), new THREE.Vector3(0, 140, 0)]); scene.add(new THREE.Line(axisGeo, axisMat)); // ----------------------------- // Satellite rendering // ----------------------------- const satGroup = new THREE.Group(); const trackGroup = new THREE.Group(); scene.add(satGroup); scene.add(trackGroup); const satGeom = new THREE.SphereGeometry(0.9, 10, 10); const satMatDefault = new THREE.MeshBasicMaterial({ color: COLORS.solarGold }); const satMatSelected = new THREE.MeshBasicMaterial({ color: COLORS.stellarCyan }); const TRACK_DEFAULT = COLORS.nebulaViolet; const TRACK_SELECTED = COLORS.solarGold; let sats = []; let selected = null; const EARTH_KM = 6371; const SCALE = R / EARTH_KM; function eciToScene(posKm){ return new THREE.Vector3(posKm.x * SCALE, posKm.z * SCALE, posKm.y * SCALE); } function clearSats(){ while (satGroup.children.length) satGroup.remove(satGroup.children[0]); while (trackGroup.children.length) trackGroup.remove(trackGroup.children[0]); sats = []; selected = null; setSelectedName("—"); updateSatCount(); renderSatList(); setSatInfoEmpty(); } function updateSatCount(){ document.getElementById("satCount").textContent = String(sats.length); } function setStatus(s){ document.getElementById("status").textContent = s; } function setSelectedName(s){ document.getElementById("selectedName").textContent = s; } function buildTrackLineECEF(satrec, minutesAhead = 90, stepMin = 2){ const pts = []; const now = new Date(); for (let m = 0; m <= minutesAhead; m += stepMin){ const t = new Date(now.getTime() + m*60*1000); const pv = satellite.propagate(satrec, t); if (!pv.position) continue; const gmst = satellite.gstime(t); const ecf = satellite.eciToEcf(pv.position, gmst); pts.push(eciToScene(ecf)); } const geo = new THREE.BufferGeometry().setFromPoints(pts); const mat = new THREE.LineBasicMaterial({ color: TRACK_DEFAULT, transparent:true, opacity:0.35 }); return new THREE.Line(geo, mat); } function addSat({name, tle1, tle2}){ const satrec = satellite.twoline2satrec(tle1, tle2); const mesh = new THREE.Mesh(satGeom, satMatDefault.clone()); mesh.userData.name = name; satGroup.add(mesh); const track = buildTrackLineECEF(satrec, 90, 2); trackGroup.add(track); sats.push({ name, tle1, tle2, satrec, mesh, trackLine: track }); } const SAMPLE_TLES = [ { name: "ISS (ZARYA)", tle1: "1 25544U 98067A 24060.53143519 .00012925 00000+0 23103-3 0 9993", tle2: "2 25544 51.6417 75.5720 0004195 93.2871 43.0120 15.50094131439139" }, { name: "HUBBLE", tle1: "1 20580U 90037B 24059.90548624 .00000679 00000+0 28557-4 0 9997", tle2: "2 20580 28.4691 50.1306 0002679 67.4635 292.6714 15.09416616307122" }, { name: "NOAA 19", tle1: "1 33591U 09005A 24060.53861383 .00000080 00000+0 77769-4 0 9993", tle2: "2 33591 99.1737 65.3426 0013872 87.7186 272.5564 14.12418440776428" }, { name: "STARLINK-30000 (DEMO)", tle1: "1 54000U 22100A 24060.50000000 .00001234 00000+0 12345-3 0 9991", tle2: "2 54000 53.0000 120.0000 0001200 80.0000 280.0000 15.05500000 9990" } ]; const satListEl = document.getElementById("satList"); const searchEl = document.getElementById("search"); function escapeHtml(str){ return str.replace(/[&<>"']/g, m => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[m])); } function renderSatList(){ satListEl.innerHTML = ""; const q = (searchEl.value || "").trim().toLowerCase(); const filtered = sats.filter(s => s.name.toLowerCase().includes(q)); for (const s of filtered){ const row = document.createElement("div"); row.className = "satItem"; row.innerHTML = `
${escapeHtml(s.name)}
TRACK
`; row.addEventListener("click", () => selectSat(s.name)); satListEl.appendChild(row); } } searchEl.addEventListener("input", renderSatList); function selectSat(name){ for (const s of sats){ s.mesh.material = satMatDefault.clone(); s.trackLine.material.opacity = 0.35; s.trackLine.material.color.setHex(TRACK_DEFAULT); s.trackLine.material.needsUpdate = true; } selected = sats.find(x => x.name === name) || null; if (!selected){ setSelectedName("—"); setSatInfoEmpty(); return; } selected.mesh.material = satMatSelected.clone(); selected.trackLine.material.opacity = 0.85; selected.trackLine.material.color.setHex(TRACK_SELECTED); selected.trackLine.material.needsUpdate = true; setSelectedName(selected.name); document.getElementById("infoName").textContent = selected.name; controls.target.set(0,0,0); controls.update(); } function setSatInfoEmpty(){ document.getElementById("infoName").textContent = "—"; document.getElementById("infoLat").textContent = "—"; document.getElementById("infoLon").textContent = "—"; document.getElementById("infoAlt").textContent = "—"; document.getElementById("infoSpd").textContent = "—"; document.getElementById("infoUtc").textContent = "—"; } function updateSelectedInfo(now){ if (!selected) return; const pv = satellite.propagate(selected.satrec, now); if (!pv.position || !pv.velocity) return; const gmst = satellite.gstime(now); const geo = satellite.eciToGeodetic(pv.position, gmst); const lat = radToDeg(geo.latitude); const lon = radToDeg(geo.longitude); const alt = geo.height; const v = pv.velocity; const spd = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); document.getElementById("infoLat").textContent = lat.toFixed(2) + "°"; document.getElementById("infoLon").textContent = lon.toFixed(2) + "°"; document.getElementById("infoAlt").textContent = alt.toFixed(0); document.getElementById("infoSpd").textContent = spd.toFixed(2); document.getElementById("infoUtc").textContent = now.toISOString().replace(".000Z","Z"); } async function loadFromEndpoint(url){ setStatus("LOADING..."); try{ const res = await fetch(url, { cache: "no-store" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const list = Array.isArray(data) ? data : (data.satellites || []); if (!Array.isArray(list) || list.length === 0) throw new Error("No satellites returned"); clearSats(); const capped = list.slice(0, 300); for (const item of capped){ if (!item?.tle1 || !item?.tle2) continue; addSat({ name: item.name || "UNKNOWN", tle1: item.tle1, tle2: item.tle2 }); } updateSatCount(); renderSatList(); setStatus("ONLINE"); }catch(e){ console.error(e); setStatus("ENDPOINT ERROR"); alert("Endpoint load failed. Use sample TLEs for now, or verify your Wix Velo function returns JSON { satellites:[{name,tle1,tle2}] }."); } } function loadSample(){ setStatus("LOADING..."); clearSats(); for (const item of SAMPLE_TLES) addSat(item); updateSatCount(); renderSatList(); setStatus("SAMPLE READY"); if (sats[0]) selectSat(sats[0].name); } document.getElementById("loadBtn").addEventListener("click", () => { const url = document.getElementById("tleUrl").value.trim(); if (!url){ alert("Paste your Wix endpoint URL first, or click 'Use Built-In Sample TLEs'."); return; } loadFromEndpoint(url); }); document.getElementById("sampleBtn").addEventListener("click", loadSample); function normLon(deg){ let x = deg % 360; if (x > 180) x -= 360; if (x < -180) x += 360; return x; } function computeLaunchWindows(){ const out = document.getElementById("launchOut"); out.value = ""; const latLon = document.getElementById("launchLatLon").value.split(",").map(s => parseFloat(s.trim())); const lat = latLon[0]; const lon = latLon[1]; const inc = parseFloat(document.getElementById("targetInc").value); const altKm = parseFloat(document.getElementById("targetAlt").value); const lan = parseFloat(document.getElementById("targetLan").value); if (![lat,lon,inc,altKm,lan].every(Number.isFinite)){ out.value = "Invalid inputs."; return; } const rotDegPerMin = 360 / (24*60); const now = new Date(); const results = []; for (let m=0; m<=48*60; m+=2){ const t = new Date(now.getTime() + m*60*1000); const siteLonAtT = normLon(lon + rotDegPerMin * m); const err = Math.abs(normLon(siteLonAtT - lan)); if (err < 0.8){ const feasible = Math.abs(lat) <= inc + 1e-6; results.push({ t, err, feasible }); m += 10; } } if (!results.length){ out.value = "No windows found in the next 48 hours with current thresholds."; return; } out.value = `Launch Site: ${lat.toFixed(4)}, ${lon.toFixed(4)}\n` + `Target: inc=${inc.toFixed(2)}°, alt=${altKm.toFixed(0)} km, LAN=${lan.toFixed(2)}°\n\n` + results.slice(0, 12).map((r,i) => { const utc = r.t.toISOString().replace(".000Z","Z"); const tag = r.feasible ? "OK" : "DOGLEG"; return `${String(i+1).padStart(2,"0")}. ${utc} (alignment ±${r.err.toFixed(2)}°) [${tag}]`; }).join("\n") + `\n\nNotes:\n- [OK] means site latitude <= inclination.\n- [DOGLEG] suggests an out-of-plane maneuver may be required.\n- This is a simplified planner for MVP visuals.`; } document.getElementById("launchBtn").addEventListener("click", computeLaunchWindows); function updateSatPositionsECEF(now){ if (!sats.length) return; const gmstNow = satellite.gstime(now); for (const s of sats){ const pv = satellite.propagate(s.satrec, now); if (!pv.position) continue; const ecf = satellite.eciToEcf(pv.position, gmstNow); s.mesh.position.copy(eciToScene(ecf)); } } let lastT = performance.now(); let infoAccum = 0; function animate(t){ const dt = Math.min(0.033, (t - lastT) / 1000); lastT = t; const now = new Date(); updateSatPositionsECEF(now); controls.update(); renderer.render(scene, camera); infoAccum += dt; if (infoAccum >= 0.25){ infoAccum = 0; updateSelectedInfo(now); } requestAnimationFrame(animate); } requestAnimationFrame(animate); loadSample();