593 lines
22 KiB
HTML
593 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Orbital Ascension Simulator | Anthony Figueroa</title>
|
|
<style>
|
|
:root {
|
|
--void: #0a0a0f;
|
|
--nebula: #1a1a2e;
|
|
--plasma: #ff6b35;
|
|
--ice: #00d9ff;
|
|
--lattice: #4a4a5a;
|
|
--text: #e8e8f0;
|
|
--accent: #ffd700;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'SF Mono', 'Menlo', monospace;
|
|
background: var(--void);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
header {
|
|
padding: 2rem;
|
|
border-bottom: 1px solid var(--lattice);
|
|
position: relative;
|
|
}
|
|
|
|
h1 {
|
|
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
|
letter-spacing: -0.02em;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.subtitle {
|
|
color: var(--plasma);
|
|
font-size: 0.9rem;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
nav {
|
|
display: flex;
|
|
gap: 2rem;
|
|
margin-top: 1.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
nav a {
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid var(--lattice);
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
nav a:hover {
|
|
border-color: var(--plasma);
|
|
background: rgba(255, 107, 53, 0.1);
|
|
}
|
|
|
|
.canvas-container {
|
|
width: 100%;
|
|
height: 70vh;
|
|
position: relative;
|
|
background: radial-gradient(ellipse at center, var(--nebula) 0%, var(--void) 100%);
|
|
overflow: hidden;
|
|
}
|
|
|
|
canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.hud {
|
|
position: absolute;
|
|
bottom: 2rem;
|
|
left: 2rem;
|
|
right: 2rem;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
z-index: 10;
|
|
}
|
|
|
|
.telemetry-panel {
|
|
background: rgba(10, 10, 15, 0.9);
|
|
border: 1px solid var(--lattice);
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.telemetry-panel h3 {
|
|
color: var(--plasma);
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
margin-bottom: 0.75rem;
|
|
border-bottom: 1px solid var(--lattice);
|
|
padding-bottom: 0.5rem;
|
|
}
|
|
|
|
.metric {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 0.5rem 0;
|
|
font-size: 0.85rem;
|
|
border-bottom: 1px dashed var(--lattice);
|
|
}
|
|
|
|
.metric:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.metric-value {
|
|
color: var(--ice);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.controls {
|
|
position: absolute;
|
|
top: 2rem;
|
|
right: 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
z-index: 10;
|
|
}
|
|
|
|
button {
|
|
background: transparent;
|
|
color: var(--text);
|
|
border: 1px solid var(--plasma);
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: 0.85rem;
|
|
transition: all 0.2s ease;
|
|
text-align: left;
|
|
}
|
|
|
|
button:hover {
|
|
background: var(--plasma);
|
|
color: var(--void);
|
|
}
|
|
|
|
button.active {
|
|
background: var(--plasma);
|
|
color: var(--void);
|
|
}
|
|
|
|
.phase-indicator {
|
|
position: absolute;
|
|
top: 2rem;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(10, 10, 15, 0.95);
|
|
border: 1px solid var(--ice);
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 50px;
|
|
font-size: 0.8rem;
|
|
color: var(--ice);
|
|
z-index: 10;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
footer {
|
|
padding: 2rem;
|
|
border-top: 1px solid var(--lattice);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.credits {
|
|
font-size: 0.8rem;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.citation-link {
|
|
color: var(--plasma);
|
|
text-decoration: none;
|
|
border-bottom: 1px dotted var(--plasma);
|
|
}
|
|
|
|
.data-link {
|
|
color: var(--ice);
|
|
text-decoration: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.hud {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.controls {
|
|
position: fixed;
|
|
bottom: 2rem;
|
|
right: 2rem;
|
|
top: auto;
|
|
}
|
|
}
|
|
</style>
|
|
<script defer src="https://analytics.4ort.xyz/script.js" data-website-id="d3ed927c-888a-4a6c-ae5f-0b1c613ddf5b"></script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Orbital Ascension Simulator</h1>
|
|
<div class="subtitle">From Duluth to LEO: A Solid-Propellant Trajectory Model</div>
|
|
<nav>
|
|
<a href="/index.html">Home</a>
|
|
<a href="/monte-carlo-winter-load.html">Monte Carlo Winter Load</a>
|
|
<a href="/orbital-ascension.json" class="data-link">Physics Schema ↗</a>
|
|
<a href="https://skyroot.in/vks.html" target="_blank" rel="noopener">Vikram-S Specs ↗</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="canvas-container" id="simulation">
|
|
<canvas id="trajectoryCanvas"></canvas>
|
|
|
|
<div class="phase-indicator" id="phaseDisplay">PHASE: PRE-LIGHT</div>
|
|
|
|
<div class="controls">
|
|
<button id="btnLiftoff" onclick="initiateSequence('liftoff')">IGNITE FIRST STAGE</button>
|
|
<button id="btnStage" onclick="initiateSequence('stage')" disabled>STAGE SEPARATION</button>
|
|
<button id="btnInjection" onclick="initiateSequence('inject')" disabled>ORBIT INJECTION</button>
|
|
<button onclick="resetSimulation()">ABORT & RESET</button>
|
|
</div>
|
|
|
|
<div class="hud">
|
|
<div class="telemetry-panel">
|
|
<h3>Vehicle State</h3>
|
|
<div class="metric"><span>ALTITUDE</span><span class="metric-value" id="altValue">0.0 km</span></div>
|
|
<div class="metric"><span>VELOCITY</span><span class="metric-value" id="velValue">0.0 m/s</span></div>
|
|
<div class="metric"><span>MASS</span><span class="metric-value" id="massValue">0.0 kg</span></div>
|
|
<div class="metric"><span>G-LOAD</span><span class="metric-value" id="gValue">0.0 g</span></div>
|
|
</div>
|
|
|
|
<div class="telemetry-panel">
|
|
<h3>Atmospheric Profile</h3>
|
|
<div class="metric"><span>DENSITY</span><span class="metric-value" id="densityValue">1.225 kg/m³</span></div>
|
|
<div class="metric"><span>TEMP</span><span class="metric-value" id="tempValue">15.0 °C</span></div>
|
|
<div class="metric"><span>PRESSURE</span><span class="metric-value" id="pressureValue">101.3 kPa</span></div>
|
|
<div class="metric"><span>DRAG COEF</span><span class="metric-value" id="dragValue">0.00</span></div>
|
|
</div>
|
|
|
|
<div class="telemetry-panel">
|
|
<h3>Engine Telemetry</h3>
|
|
<div class="metric"><span>THRUST</span><span class="metric-value" id="thrustValue">0.0 kN</span></div>
|
|
<div class="metric"><span>ISP</span><span class="metric-value" id="ispValue">0 s</span></div>
|
|
<div class="metric"><span>FLOW RATE</span><span class="metric-value" id="flowValue">0.0 kg/s</span></div>
|
|
<div class="metric"><span>TIME ELAPSED</span><span class="metric-value" id="timeValue">00:00</span></div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<footer>
|
|
<div class="credits">
|
|
Grounded in <a href="https://4ort.xyz/entity/vikram-s" class="citation-link" target="_blank" rel="noopener">Vikram-S (Q115269876)</a> • Skyroot Aerospace • Service Entry: 2022-11-18
|
|
</div>
|
|
<div class="credits">
|
|
<a href="/orbital-ascension.json" class="data-link">Agent-Legible Physics Twin ↗</a>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
// PHYSICS CONSTANTS FROM VIKRAM-S MISSION PROFILE
|
|
const PHYSICS = {
|
|
// Initial vehicle state (solid propellant sounding rocket baseline)
|
|
dryMass: 1200, // kg - estimated dry mass
|
|
propellantMass: 8500, // kg - solid propellant load
|
|
totalInitialMass: 9700, // kg
|
|
|
|
// Engine performance (typical solid motor values)
|
|
ispVacuum: 265, // seconds - vacuum specific impulse
|
|
ispSeaLevel: 235, // seconds - sea level ISP
|
|
thrustSeaLevel: 1800, // kN - approximate liftoff thrust
|
|
thrustVacuum: 2100, // kN - vacuum thrust
|
|
burnDuration: 62, // seconds - first stage burn time
|
|
|
|
// Aerodynamic properties
|
|
diameter: 2.4, // meters - vehicle diameter
|
|
length: 28, // meters - total length
|
|
dragCoefficient: 0.35, // dimensionless
|
|
finenessRatio: 11.67, // length/diameter
|
|
|
|
// Atmospheric model (U.S. Standard Atmosphere 1976 simplified)
|
|
seaLevelDensity: 1.225, // kg/m³
|
|
scaleHeight: 8500, // meters - atmospheric scale height
|
|
seaLevelTemp: 288.15, // Kelvin
|
|
lapseRate: -0.0065, // K/m - troposphere lapse rate
|
|
|
|
// Gravity
|
|
g0: 9.80665, // m/s² - standard gravity
|
|
earthRadius: 6371000, // meters
|
|
mu: 3.986004418e14, // m³/s² - gravitational parameter
|
|
|
|
// Mission profile targets
|
|
targetAltitude: 300000, // meters - 300km orbit
|
|
targetVelocity: 7700, // m/s - circular orbit velocity at 300km
|
|
stagingAltitude: 85000, // meters - approximate staging point
|
|
stagingVelocity: 2400, // m/s - velocity at staging
|
|
};
|
|
|
|
// Simulation state
|
|
let simState = {
|
|
t: 0, // elapsed time (seconds)
|
|
alt: 0, // altitude (meters)
|
|
vel: 0, // velocity (m/s)
|
|
mass: PHYSICS.totalInitialMass,
|
|
thrust: 0, // current thrust (Newtons)
|
|
isp: 0, // current ISP
|
|
flowRate: 0, // propellant mass flow (kg/s)
|
|
gLoad: 0, // acceleration in g's
|
|
phase: 'PRELIGHT', // PRELIGHT -> ASCENT -> STAGING -> INJECTION -> ORBIT
|
|
running: false
|
|
};
|
|
|
|
// Canvas setup
|
|
const canvas = document.getElementById('trajectoryCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
let animationId;
|
|
|
|
function resizeCanvas() {
|
|
canvas.width = canvas.parentElement.clientWidth;
|
|
canvas.height = canvas.parentElement.clientHeight;
|
|
}
|
|
|
|
window.addEventListener('resize', resizeCanvas);
|
|
resizeCanvas();
|
|
|
|
// Atmospheric model
|
|
function getAtmosphere(altitude) {
|
|
const density = PHYSICS.seaLevelDensity * Math.exp(-altitude / PHYSICS.scaleHeight);
|
|
const temp = Math.max(180, PHYSICS.seaLevelTemp + PHYSICS.lapseRate * altitude);
|
|
const pressure = 101325 * Math.exp(-altitude / PHYSICS.scaleHeight);
|
|
return { density, temp: temp - 273.15, pressure: pressure / 1000 };
|
|
}
|
|
|
|
// Rocket equation integration
|
|
function computeThrustCycle(dt) {
|
|
const atm = getAtmosphere(simState.alt);
|
|
const altFraction = Math.min(1, simState.alt / PHYSICS.stagingAltitude);
|
|
|
|
// Linear ISP interpolation (sea level -> vacuum)
|
|
simState.isp = PHYSICS.ispSeaLevel +
|
|
(PHYSICS.ispVacuum - PHYSICS.ispSeaLevel) * altFraction;
|
|
|
|
// Thrust scales slightly with ambient pressure
|
|
const pressureFactor = 1 + 0.1 * (1 - atm.pressure / 101.3);
|
|
simState.thrust = PHYSICS.thrustSeaLevel * 1000 * pressureFactor;
|
|
|
|
// Mass flow from T = ṁ * Isp * g₀
|
|
simState.flowRate = simState.thrust / (simState.isp * PHYSICS.g0);
|
|
}
|
|
|
|
// Euler integration step
|
|
function integrateStep(dt) {
|
|
if (!simState.running || simState.phase === 'PRELIGHT') return;
|
|
|
|
computeThrustCycle(dt);
|
|
|
|
// Mass depletion
|
|
const dm = simState.flowRate * dt;
|
|
simState.mass = Math.max(PHYSICS.dryMass, simState.mass - dm);
|
|
|
|
// Forces
|
|
const weight = simState.mass * PHYSICS.mu / Math.pow(PHYSICS.earthRadius + simState.alt, 2);
|
|
const area = Math.PI * Math.pow(PHYSICS.diameter / 2, 2);
|
|
const atm = getAtmosphere(simState.alt);
|
|
const drag = 0.5 * atm.density * simState.vel * simState.vel *
|
|
PHYSICS.dragCoefficient * area;
|
|
|
|
// Net acceleration
|
|
const netForce = simState.thrust - weight - drag;
|
|
const accel = netForce / simState.mass;
|
|
|
|
// Update kinematics
|
|
simState.vel += accel * dt;
|
|
simState.alt += simState.vel * dt;
|
|
simState.t += dt;
|
|
|
|
// G-load
|
|
simState.gLoad = accel / PHYSICS.g0;
|
|
|
|
// Phase transitions
|
|
if (simState.phase === 'ASCENT' &&
|
|
simState.alt >= PHYSICS.stagingAltitude * 0.95 &&
|
|
simState.t >= PHYSICS.burnDuration * 0.9) {
|
|
triggerStaging();
|
|
}
|
|
|
|
if (simState.phase === 'INJECTION' &&
|
|
simState.vel >= PHYSICS.targetVelocity * 0.98 &&
|
|
simState.alt >= PHYSICS.targetAltitude * 0.95) {
|
|
achieveOrbit();
|
|
}
|
|
}
|
|
|
|
function triggerStaging() {
|
|
simState.phase = 'STAGING';
|
|
document.getElementById('phaseDisplay').textContent = 'PHASE: STAGE SEPARATION';
|
|
document.getElementById('btnStage').classList.add('active');
|
|
|
|
// Simulate staging event (dry mass reduction)
|
|
setTimeout(() => {
|
|
simState.mass = PHYSICS.dryMass; // Second stage dry mass
|
|
simState.phase = 'INJECTION';
|
|
document.getElementById('phaseDisplay').textContent = 'PHASE: ORBIT INJECTION';
|
|
document.getElementById('btnStage').classList.remove('active');
|
|
document.getElementById('btnInjection').classList.add('active');
|
|
}, 2000);
|
|
}
|
|
|
|
function achieveOrbit() {
|
|
simState.phase = 'ORBIT';
|
|
document.getElementById('phaseDisplay').textContent = 'PHASE: ORBIT ACHIEVED';
|
|
document.getElementById('btnInjection').classList.remove('active');
|
|
simState.running = false;
|
|
}
|
|
|
|
// Visualization
|
|
function drawTrajectory() {
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
|
|
// Clear
|
|
ctx.fillStyle = '#0a0a0f';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
// Draw Earth curvature
|
|
const earthCenterY = h + PHYSICS.earthRadius * (h / PHYSICS.targetAltitude);
|
|
ctx.beginPath();
|
|
ctx.arc(w / 2, earthCenterY, PHYSICS.earthRadius * (h / PHYSICS.targetAltitude),
|
|
Math.PI, 0, false);
|
|
ctx.strokeStyle = '#00d9ff';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
|
|
// Draw trajectory arc (parabolic approximation)
|
|
if (simState.alt > 0) {
|
|
const xScale = w / (PHYSICS.targetVelocity * PHYSICS.burnDuration * 2);
|
|
const yScale = h / PHYSICS.targetAltitude;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, h);
|
|
|
|
// Quadratic Bezier approximating the ascent curve
|
|
const controlX = simState.vel * xScale * 0.5;
|
|
const controlY = h - simState.alt * yScale * 0.5;
|
|
const endX = simState.vel * xScale;
|
|
const endY = h - simState.alt * yScale;
|
|
|
|
ctx.quadraticCurveTo(controlX, controlY, endX, endY);
|
|
|
|
ctx.strokeStyle = '#ff6b35';
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
|
|
// Vehicle marker
|
|
ctx.beginPath();
|
|
ctx.arc(endX, endY, 6, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#ffd700';
|
|
ctx.fill();
|
|
}
|
|
|
|
// Grid overlay
|
|
ctx.strokeStyle = 'rgba(74, 74, 90, 0.3)';
|
|
ctx.lineWidth = 1;
|
|
for (let i = 0; i < 10; i++) {
|
|
const y = h - (i * h / 10);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(w, y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// HUD updates
|
|
function updateHUD() {
|
|
document.getElementById('altValue').textContent = (simState.alt / 1000).toFixed(1) + ' km';
|
|
document.getElementById('velValue').textContent = simState.vel.toFixed(0) + ' m/s';
|
|
document.getElementById('massValue').textContent = (simState.mass).toFixed(0) + ' kg';
|
|
document.getElementById('gValue').textContent = simState.gLoad.toFixed(2) + ' g';
|
|
|
|
const atm = getAtmosphere(simState.alt);
|
|
document.getElementById('densityValue').textContent = atm.density.toFixed(3) + ' kg/m³';
|
|
document.getElementById('tempValue').textContent = atm.temp.toFixed(1) + ' °C';
|
|
document.getElementById('pressureValue').textContent = atm.pressure.toFixed(1) + ' kPa';
|
|
|
|
document.getElementById('thrustValue').textContent = (simState.thrust / 1000).toFixed(0) + ' kN';
|
|
document.getElementById('ispValue').textContent = simState.isp.toFixed(0) + ' s';
|
|
document.getElementById('flowValue').textContent = simState.flowRate.toFixed(1) + ' kg/s';
|
|
|
|
const minutes = Math.floor(simState.t / 60);
|
|
const seconds = Math.floor(simState.t % 60);
|
|
document.getElementById('timeValue').textContent =
|
|
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
// Main loop
|
|
function gameLoop() {
|
|
if (simState.running) {
|
|
const dt = 0.05; // 20 Hz update rate
|
|
for (let i = 0; i < 4; i++) {
|
|
integrateStep(dt);
|
|
}
|
|
}
|
|
|
|
drawTrajectory();
|
|
updateHUD();
|
|
|
|
animationId = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Control sequences
|
|
function initiateSequence(stage) {
|
|
switch(stage) {
|
|
case 'liftoff':
|
|
simState.phase = 'ASCENT';
|
|
simState.running = true;
|
|
document.getElementById('phaseDisplay').textContent = 'PHASE: MAIN ENGINE ASCENT';
|
|
document.getElementById('btnLiftoff').classList.add('active');
|
|
document.getElementById('btnStage').disabled = false;
|
|
break;
|
|
|
|
case 'stage':
|
|
if (simState.phase === 'ASCENT') {
|
|
triggerStaging();
|
|
}
|
|
break;
|
|
|
|
case 'inject':
|
|
if (simState.phase === 'INJECTION') {
|
|
// Boost to orbital velocity
|
|
const boostDuration = 180; // 3 minutes upper stage burn
|
|
const dvNeeded = PHYSICS.targetVelocity - simState.vel;
|
|
simState.thrust *= 0.3; // Upper stage lower thrust
|
|
simState.isp = PHYSICS.ispVacuum;
|
|
|
|
setTimeout(() => {
|
|
achieveOrbit();
|
|
}, boostDuration * 1000);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function resetSimulation() {
|
|
cancelAnimationFrame(animationId);
|
|
simState = {
|
|
t: 0,
|
|
alt: 0,
|
|
vel: 0,
|
|
mass: PHYSICS.totalInitialMass,
|
|
thrust: 0,
|
|
isp: 0,
|
|
flowRate: 0,
|
|
gLoad: 0,
|
|
phase: 'PRELIGHT',
|
|
running: false
|
|
};
|
|
|
|
document.getElementById('phaseDisplay').textContent = 'PHASE: PRE-LIGHT';
|
|
document.getElementById('btnLiftoff').classList.remove('active');
|
|
document.getElementById('btnStage').classList.remove('active');
|
|
document.getElementById('btnStage').disabled = true;
|
|
document.getElementById('btnInjection').classList.remove('active');
|
|
document.getElementById('btnInjection').disabled = true;
|
|
|
|
gameLoop();
|
|
}
|
|
|
|
// Initialize
|
|
gameLoop();
|
|
</script>
|
|
</body>
|
|
</html>
|