$sloprun.dev

Delta-V Calculator

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 6 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

Delta-v is the only currency a rocket has: a speed budget it spends climbing, turning and stopping, and when it's gone the trip is over. This one opens preloaded with a single-stage hydrogen rocket that clears low orbit by 92 m/s — a margin a real flight would burn through on the way up — so the first thing you want to do is fix it.

Drag propellant, dry mass or exhaust velocity and the big number moves instantly, while a budget bar fills toward four mileposts (low orbit, higher orbit, lunar flyby, lunar landing) and turns green only for the ones actually cleared. Each stage shows its own substituted equation, so you can watch the logarithm quietly eat your extra tonnes.

Try this first: click "same mass, staged". It is the same 147 t sitting on the pad as the default rocket, just split into two stages — and it gains about 1,980 m/s for free, purely by dropping empty tanks instead of hauling them uphill. Then try beating that by adding propellant to the single-stage version instead, and see how much mass it costs you. Numbers are the ideal rocket equation only; drag and gravity losses are folded into the mileposts, not into the rocket.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/rocket-equation" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Delta-V Calculator — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/rocket-equation

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 787 lines 35.9 KB index.html
demos/rocket-equation/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Delta-V Calculator</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }

/* ---- demo-local colours, derived from the tokens above ---- */
:root {
  --c-prop: #C05600;      /* propellant — the amber that burns */
  --c-prop2: #E7A468;     /* propellant, upper stage */
  --c-dry: #8C949E;       /* empty structure */
  --c-dry2: #C4CAD1;      /* empty structure, upper stage */
  --c-pay: #1A1D21;       /* the thing you are actually delivering */
  --shadow: 0 1px 2px rgba(20,22,26,.06), 0 8px 24px rgba(20,22,26,.05);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --c-prop: #FFB454; --c-prop2: #9A672C;
    --c-dry: #6E7784; --c-dry2: #434A55;
    --c-pay: #E8E6E1;
    --shadow: none;
  }
}
:root[data-theme="dark"] {
  --c-prop: #FFB454; --c-prop2: #9A672C;
  --c-dry: #6E7784; --c-dry2: #434A55;
  --c-pay: #E8E6E1;
  --shadow: none;
}

/* ---- base ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { line-height: 1.5; -webkit-text-size-adjust: 100%; overflow-x: hidden; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
      clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

.wrap { max-width: 1080px; margin: 0 auto; padding: 22px 16px 40px; }

header.top { margin-bottom: 18px; }
.kicker { font-family: var(--font-mono); font-size: 11px; letter-spacing: .09em;
          text-transform: uppercase; color: var(--muted); margin: 0 0 6px; }
h1 { font-size: clamp(1.45rem, 4.4vw, 2rem); line-height: 1.15; margin: 0 0 6px;
     letter-spacing: -.02em; font-weight: 650; }
.lede { margin: 0; color: var(--muted); font-size: .97rem; max-width: 58ch; }

.layout { display: grid; gap: 14px; grid-template-columns: minmax(0,1fr);
           grid-template-areas: "a" "panel" "b"; }
@media (min-width: 940px) {
  .layout { grid-template-columns: minmax(0,1fr) 300px;
            grid-template-areas: "a panel" "b panel"; }
}
.col { display: grid; gap: 14px; min-width: 0; align-content: start; }
.colA { grid-area: a; } .colB { grid-area: b; } .panel { grid-area: panel; align-self: start; }

.card { background: var(--surface); border: 1px solid var(--line);
        border-radius: var(--radius); box-shadow: var(--shadow); }
.eyebrow { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .09em;
           text-transform: uppercase; color: var(--muted); margin: 0; font-weight: 500; }

/* ---- hero ---- */
.hero { padding: 16px 18px 16px; border-left: 3px solid var(--accent); transition: none; }
.hero.ok { border-left-color: var(--run); }
.hero-n { margin: 4px 0 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums;
          font-size: clamp(2.6rem, 12vw, 4.2rem); line-height: 1.02; font-weight: 650;
          letter-spacing: -.04em; color: var(--accent); word-break: break-word; }
.hero.ok .hero-n { color: var(--run); }
.hero-n .u { font-size: .3em; font-weight: 600; letter-spacing: .02em; margin-left: .22em;
             color: var(--muted); }
.hero-sub { margin: 6px 0 0; font-size: .97rem; max-width: 56ch; }
.hero-sub b { font-weight: 650; }
.hero-sub .n, .hero-hint .n { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
                              font-weight: 600; }
.hero-hint { margin: 6px 0 0; font-size: .87rem; color: var(--muted); max-width: 56ch; }
.hero-hint .n { color: var(--ink); }

/* ---- budget bar ---- */
.budget { padding: 14px 16px 16px; min-width: 0;
          container-type: inline-size; container-name: budget; }
.bhead { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline;
         justify-content: space-between; margin-bottom: 14px; }
.over { font-family: var(--font-mono); font-size: 11px; color: var(--accent); }

.bband { position: relative; margin: 0 0 6px; }
.blab { position: relative; height: 20px; }
.blab .lab { position: absolute; bottom: 0; display: flex; align-items: center; gap: 5px;
             white-space: nowrap; font-size: 11px; color: var(--muted); }
.blab.below .lab { top: 0; bottom: auto; }
.blab .pin { width: 15px; height: 15px; line-height: 13px; font-size: 9px; }
.blab .pin.on { background: var(--run); border-color: var(--run); color: var(--surface); }
/* the words only fit once the bar itself is wide — until then keep the numbered
   pins alone, since the list underneath already names every milepost */
.blab .lab { transform: translateX(-50%); }
.blab .nm { display: none; }
@container budget (min-width: 640px) {
  .blab .nm { display: inline; }
  .lab.c { transform: translateX(-50%); }
  .lab.r { transform: translateX(-100%); }
  .lab.l { transform: none; }
}

.track { position: relative; height: 26px; background: var(--line); border-radius: 6px;
         overflow: hidden; }
.fill { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--accent); }
.earned { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--run); }
.tick { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--surface); }
@media (prefers-reduced-motion: no-preference) {
  .fill, .earned { transition: width .2s cubic-bezier(.4,0,.2,1); }
}
.scaleends { display: flex; justify-content: space-between; margin: 5px 0 0;
             font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); }

.marks { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 9px; }
.mark { display: grid; grid-template-columns: 20px minmax(0,1fr) auto; gap: 1px 10px;
        align-items: baseline; padding-top: 9px; border-top: 1px solid var(--line); }
.mark:first-child { border-top: 0; padding-top: 0; }
.pin { grid-row: span 2; align-self: start; width: 19px; height: 19px; border-radius: 50%;
       border: 1px solid var(--line); color: var(--muted); background: var(--surface);
       font-family: var(--font-mono); font-size: 10.5px; line-height: 17px; text-align: center; }
.mark.on .pin { background: var(--run); border-color: var(--run); color: #fff; }
:root[data-theme="dark"] .mark.on .pin { color: #10240F; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .mark.on .pin { color: #10240F; }
}
.mk-name { font-size: .95rem; color: var(--muted); }
.mark.on .mk-name { color: var(--ink); font-weight: 600; }
.mk-dv { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: .85rem;
         color: var(--muted); }
.mk-note { grid-column: 2 / 4; font-size: .82rem; color: var(--muted); }
.mk-note b { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600; }
.mark.on .mk-note b { color: var(--run); }

/* ---- mass stack ---- */
.stackcard { padding: 14px 16px 15px; min-width: 0; }
.stack { display: flex; height: 30px; border-radius: 6px; overflow: hidden;
         background: var(--line); margin-top: 10px; }
.stack span { display: block; height: 100%; min-width: 0; }
.sg-p1 { background: var(--c-prop); }
.sg-d1 { background: var(--c-dry); }
.sg-p2 { background: var(--c-prop2); }
.sg-d2 { background: var(--c-dry2); }
.sg-pay { background: var(--c-pay); }
@media (prefers-reduced-motion: no-preference) { .stack span { transition: width .2s ease; } }
.legend { display: flex; flex-wrap: wrap; gap: 5px 14px; list-style: none; margin: 10px 0 0;
          padding: 0; }
.legend li { display: flex; align-items: center; gap: 6px; font-family: var(--font-mono);
             font-size: 11px; color: var(--muted); }
.sw { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.stats { display: flex; flex-wrap: wrap; gap: 4px 20px; margin: 11px 0 0; padding: 0;
         list-style: none; }
.stats li { font-size: .82rem; color: var(--muted); }
.stats b { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: .9rem;
           color: var(--ink); font-weight: 600; display: block; }

/* ---- equation rows ---- */
.eqcard { padding: 14px 16px 15px; min-width: 0; }
.eqrow { border-top: 1px solid var(--line); padding: 11px 0 0; margin-top: 11px; }
.eqrow:first-of-type { border-top: 0; padding-top: 0; margin-top: 10px; }
.eq-h { display: flex; flex-wrap: wrap; gap: 3px 10px; align-items: baseline;
        justify-content: space-between; }
.eq-t { font-family: var(--font-mono); font-size: 11px; letter-spacing: .07em;
        text-transform: uppercase; color: var(--muted); }
.eq-r { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.eq-m { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: .87rem;
        margin: 4px 0 0; color: var(--muted); overflow-x: auto; white-space: nowrap;
        padding-bottom: 2px; }
.eq-m b { color: var(--ink); font-weight: 600; }
.eq-m .res { color: var(--accent); font-weight: 650; }
.eq-m .res { white-space: nowrap; }
@media (max-width: 620px) { .eq-m { white-space: normal; overflow-x: visible; } }
.why { margin: 13px 0 0; padding: 9px 11px; border-left: 2px solid var(--accent);
       background: var(--accent-soft); border-radius: 0 6px 6px 0; font-size: .86rem; }
.caveat { margin: 0; font-size: .82rem; color: var(--muted); }
.caveat b { color: var(--ink); font-weight: 600; }

/* ---- controls ---- */
.panel { padding: 14px 14px 16px; }
@media (min-width: 940px) {
  .panel { position: sticky; top: 12px; max-height: calc(100vh - 24px); overflow-y: auto; }
}
.panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
              margin-bottom: 10px; }
.btn { font-family: var(--font-mono); font-size: 11px; color: var(--muted); background: none;
       border: 1px solid var(--line); border-radius: 7px; padding: 5px 9px; cursor: pointer;
       text-align: left; }
.btn:hover { color: var(--accent); border-color: var(--accent); }
.btn.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft);
              font-weight: 600; }
.scenarios { display: grid; gap: 6px; margin-bottom: 16px; }
.ctrl { margin-bottom: 14px; }
.ctrl:last-child { margin-bottom: 0; }
.row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.row label { font-size: .84rem; color: var(--muted); }
.row output { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
              font-size: .92rem; font-weight: 600; color: var(--ink); }
.sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); margin-top: 2px;
       display: block; }
input[type=range] { -webkit-appearance: none; appearance: none; width: 100%; height: 26px;
                    background: transparent; cursor: pointer; margin: 2px 0 0; display: block; }
input[type=range]::-webkit-slider-runnable-track { height: 4px; background: var(--line); border-radius: 99px; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 17px; height: 17px;
  margin-top: -6.5px; border-radius: 50%; background: var(--surface); border: 2px solid var(--accent); }
input[type=range]::-moz-range-track { height: 4px; background: var(--line); border-radius: 99px; }
input[type=range]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--accent); }
input[type=range]:focus-visible { outline: none; }
input[type=range]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--accent-soft), 0 0 0 5px var(--accent); }
input[type=range]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px var(--accent-soft), 0 0 0 5px var(--accent); }
select { width: 100%; margin-top: 5px; font-family: var(--font-mono); font-size: 11.5px;
         color: var(--ink); background: var(--surface); border: 1px solid var(--line);
         border-radius: 8px; padding: 7px 8px; cursor: pointer; }
select:hover { border-color: var(--accent); }
fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
fieldset[disabled] { display: none; }
.grp { border-top: 1px solid var(--line); margin-top: 16px; padding-top: 13px; }
.grp-t { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .09em;
         text-transform: uppercase; color: var(--muted); margin: 0 0 10px;
         display: flex; align-items: center; gap: 7px; }
.chip { width: 9px; height: 9px; border-radius: 3px; flex: none; background: var(--c-prop); }
.chip.two { background: var(--c-prop2); }

.toggle { display: flex; align-items: center; gap: 10px; border-top: 1px solid var(--line);
          margin-top: 16px; padding-top: 14px; }
.toggle input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.toggle label { display: flex; align-items: center; gap: 10px; cursor: pointer; min-height: 24px;
                font-size: .88rem; color: var(--ink); }
.sw2 { position: relative; width: 36px; height: 20px; border-radius: 99px; flex: none;
       background: var(--line); border: 1px solid var(--line); }
.sw2::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
              border-radius: 50%; background: var(--surface); box-shadow: 0 1px 2px rgba(0,0,0,.25); }
.toggle input:checked + label .sw2 { background: var(--accent); border-color: var(--accent); }
.toggle input:checked + label .sw2::after { left: 18px; }
.toggle input:focus-visible + label .sw2 { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: no-preference) {
  .sw2, .sw2::after { transition: background .15s ease, left .15s ease; }
}

.foot { margin-top: 22px; font-family: var(--font-mono); font-size: 11px; color: var(--muted);
        letter-spacing: .04em; }
</style>
</head>
<body>
<div class="wrap">

  <header class="top">
    <p class="kicker">aerospace · runs in your browser</p>
    <h1>How much speed does this rocket actually have?</h1>
    <p class="lede">Delta-v is a rocket's whole speed budget — spend it climbing, spend it
      steering, and when it's gone, it's gone. Drag the masses and watch how far the same
      hardware can reach.</p>
  </header>

  <div class="layout">
    <div class="col colA">

      <section class="card hero ok" id="hero" aria-labelledby="heroTitle">
        <p class="eyebrow" id="heroTitle">Total delta-v</p>
        <p class="hero-n mono" id="dvBig">9,492<span class="u">m/s</span></p>
        <p class="hero-sub" id="verdict"><b>Just barely</b> enough to reach a low orbit — <span class="n">92 m/s</span> of margin, which a real flight would eat on the way up.</p>
        <p class="hero-hint" id="hint">Turn on the second stage to see what dropping empty tanks is worth.</p>
      </section>

      <section class="card budget" aria-labelledby="budgetTitle">
        <div class="bhead">
          <p class="eyebrow" id="budgetTitle">How far that gets you</p>
          <p class="over mono" id="over" hidden></p>
        </div>

        <div class="bband">
          <div class="blab above" id="labAbove" aria-hidden="true"></div>
          <div class="track" id="track" role="img" aria-label="delta-v budget bar">
            <div class="fill" id="fill"></div>
            <div class="earned" id="earned"></div>
          </div>
          <div class="blab below" id="labBelow" aria-hidden="true"></div>
          <div class="scaleends" aria-hidden="true"><span>0</span><span id="scaleMax">16,000 m/s</span></div>
        </div>

        <ul class="marks" id="marks"></ul>
      </section>
    </div>

    <div class="col colB">
      <section class="card stackcard" aria-labelledby="stackTitle">
        <p class="eyebrow" id="stackTitle">What you are lifting</p>
        <div class="stack" id="stack" aria-hidden="true"></div>
        <ul class="legend" id="legend" aria-hidden="true"></ul>
        <ul class="stats">
          <li>mass at liftoff<b id="stM0">147 t</b></li>
          <li>propellant share<b id="stProp">88.4%</b></li>
          <li>payload<b id="stPay">5 t</b></li>
          <li>payload fraction<b id="stFrac">3.4%</b></li>
        </ul>
      </section>

      <section class="card eqcard" aria-labelledby="eqTitle">
        <p class="eyebrow" id="eqTitle">The rocket equation, stage by stage</p>
        <div id="eqRows"></div>
        <p class="why">Delta-v grows with the <b>logarithm</b> of the mass ratio: the propellant
          at the bottom has to accelerate all the propellant above it, so each extra tonne buys
          less speed than the last — which is why dropping an empty stage, instead of dragging
          its dead tanks along, is worth more than simply filling bigger tanks.</p>
      </section>

      <p class="caveat"><b>Simplified on purpose.</b> This is the ideal rocket equation only —
        no drag, no gravity losses, no steering or throttling. Real climbs bleed roughly
        1.5–2 km/s on the way up, which is already folded into the mileposts above, so treat
        every number here as a back-of-the-envelope teaching figure rather than a flight plan.</p>

      <p class="sr" id="live" aria-live="polite"></p>
    </div>

    <form class="card panel" id="controls" autocomplete="off">
      <div class="panel-head">
        <p class="eyebrow">Build a rocket</p>
        <button type="button" class="btn" id="reset">reset</button>
      </div>

      <div class="scenarios" id="scenarios"></div>

      <div class="ctrl">
        <div class="row"><label for="payload">Payload</label>
          <output class="mono" id="oPayload" for="payload">5 t</output></div>
        <input type="range" id="payload" min="0" max="20" step="0.5" value="5">
        <span class="sub">the part that is not rocket</span>
      </div>

      <fieldset class="grp" id="s1fs">
        <p class="grp-t"><span class="chip" aria-hidden="true"></span>Stage 1 — fires first</p>

        <div class="ctrl">
          <label class="sub" style="margin:0" for="eng1">Engine type · exhaust m/s</label>
          <select id="eng1"></select>
        </div>

        <div class="ctrl">
          <div class="row"><label for="ve1">Exhaust velocity</label>
            <output class="mono" id="oVe1" for="ve1">4,400 m/s</output></div>
          <input type="range" id="ve1" min="1500" max="4600" step="10" value="4400">
          <span class="sub" id="sIsp1">≈ 449 s specific impulse</span>
        </div>

        <div class="ctrl">
          <div class="row"><label for="prop1">Propellant mass</label>
            <output class="mono" id="oProp1" for="prop1">130 t</output></div>
          <input type="range" id="prop1" min="10" max="500" step="5" value="130">
        </div>

        <div class="ctrl">
          <div class="row"><label for="dry1">Dry mass</label>
            <output class="mono" id="oDry1" for="dry1">12 t</output></div>
          <input type="range" id="dry1" min="2" max="40" step="0.5" value="12">
          <span class="sub">tanks, engines, plumbing</span>
        </div>
      </fieldset>

      <div class="toggle">
        <input type="checkbox" id="two">
        <label for="two"><span class="sw2" aria-hidden="true"></span>Add a second stage</label>
      </div>

      <fieldset class="grp" id="s2fs" disabled>
        <p class="grp-t"><span class="chip two" aria-hidden="true"></span>Stage 2 — fires after separation</p>

        <div class="ctrl">
          <label class="sub" style="margin:0" for="eng2">Engine type · exhaust m/s</label>
          <select id="eng2"></select>
        </div>

        <div class="ctrl">
          <div class="row"><label for="ve2">Exhaust velocity</label>
            <output class="mono" id="oVe2" for="ve2">4,400 m/s</output></div>
          <input type="range" id="ve2" min="1500" max="4600" step="10" value="4400">
          <span class="sub" id="sIsp2">≈ 449 s specific impulse</span>
        </div>

        <div class="ctrl">
          <div class="row"><label for="prop2">Propellant mass</label>
            <output class="mono" id="oProp2" for="prop2">30 t</output></div>
          <input type="range" id="prop2" min="2" max="150" step="1" value="30">
        </div>

        <div class="ctrl">
          <div class="row"><label for="dry2">Dry mass</label>
            <output class="mono" id="oDry2" for="dry2">3 t</output></div>
          <input type="range" id="dry2" min="1" max="20" step="0.5" value="3">
        </div>
      </fieldset>
    </form>
  </div>

  <p class="foot">rocket-equation · sloprun.dev</p>
</div>

<script>
(function () {
  "use strict";

  /* platform theme bridge (also works standalone via prefers-color-scheme) */
  window.addEventListener("message", function (e) {
    var d = e && e.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      document.documentElement.setAttribute("data-theme", d.theme);
    }
  });

  var $ = function (id) { return document.getElementById(id); };
  var G0 = 9.80665;
  var SCALE = 16000;               /* m/s spanned by the budget bar */

  var nf0 = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
  var nf1 = new Intl.NumberFormat("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
  function ms(v) { return nf0.format(Math.round(v)) + " m/s"; }
  function t(v) { return (v % 1 ? nf1.format(v) : nf0.format(v)) + " t"; }

  /* ---------- engines: civilian propellant combinations ---------- */
  var ENGINES = [
    { id: "mono",     name: "Monopropellant thruster",   ve: 2200 },
    { id: "solid",    name: "Solid propellant",          ve: 2450 },
    { id: "kerolox",  name: "Kerosene + liquid oxygen",  ve: 3050 },
    { id: "methalox", name: "Methane + liquid oxygen",   ve: 3400 },
    { id: "hydrolox", name: "Hydrogen + liquid oxygen",  ve: 4400 }
  ];
  function engineName(ve) {
    for (var i = 0; i < ENGINES.length; i++) if (ENGINES[i].ve === ve) return ENGINES[i].name;
    return "Custom mix";
  }
  function fillSelect(sel) {
    var html = "";
    for (var i = 0; i < ENGINES.length; i++) {
      html += '<option value="' + ENGINES[i].ve + '">' + ENGINES[i].name +
              " · " + ENGINES[i].ve + "</option>";
    }
    html += '<option value="custom" disabled>Custom mix (set by the slider)</option>';
    sel.innerHTML = html;
  }

  /* ---------- where the delta-v gets you (typical civilian budgets) ---------- */
  var MARKS = [
    { n: 1, name: "low orbit",     dv: 9400,
      note: "climb out of the thick air and go fast enough sideways to keep missing the ground" },
    { n: 2, name: "higher orbit",  dv: 11800,
      note: "stretch the far side of the orbit all the way out to geostationary height, 36,000 km up" },
    { n: 3, name: "lunar flyby",   dv: 12500,
      note: "a free-return loop around the Moon and back without stopping" },
    { n: 4, name: "lunar landing", dv: 15200,
      note: "brake into orbit around the Moon, then all the way down to the surface" }
  ];

  /* ---------- state ---------- */
  var SCENARIOS = [
    { id: "barely", label: "barely orbital",
      s: { payload: 5, ve1: 4400, prop1: 130, dry1: 12, two: false, ve2: 4400, prop2: 30, dry2: 3 } },
    { id: "staged", label: "same mass, staged",
      s: { payload: 5, ve1: 4400, prop1: 100, dry1: 8, two: true, ve2: 4400, prop2: 30, dry2: 4 } },
    { id: "moon", label: "two-stage moonshot",
      s: { payload: 5, ve1: 3050, prop1: 320, dry1: 18, two: true, ve2: 4400, prop2: 70, dry2: 6 } },
    { id: "solids", label: "solids alone",
      s: { payload: 2, ve1: 2450, prop1: 90, dry1: 8, two: false, ve2: 4400, prop2: 30, dry2: 3 } }
  ];
  var DEFAULTS = SCENARIOS[0].s;
  var s = Object.assign({}, DEFAULTS);

  var IN = {
    payload: $("payload"), ve1: $("ve1"), prop1: $("prop1"), dry1: $("dry1"),
    ve2: $("ve2"), prop2: $("prop2"), dry2: $("dry2")
  };

  /* ---------- math ---------- */
  function calc() {
    var stages = [];
    var b0 = s.payload + s.dry2 + s.prop2;       /* stage 2 at ignition */
    var bf = s.payload + s.dry2;                 /* stage 2 burnt out */
    var above = s.two ? b0 : s.payload;          /* everything stage 1 must also push */
    var a0 = above + s.dry1 + s.prop1;
    var af = above + s.dry1;
    stages.push({ i: 1, ve: s.ve1, m0: a0, mf: af, dv: s.ve1 * Math.log(a0 / af),
                  prop: s.prop1, dry: s.dry1 });
    if (s.two) {
      stages.push({ i: 2, ve: s.ve2, m0: b0, mf: bf, dv: s.ve2 * Math.log(b0 / bf),
                    prop: s.prop2, dry: s.dry2 });
    }
    var total = 0;
    for (var i = 0; i < stages.length; i++) total += stages[i].dv;
    var liftoff = a0;
    var propTotal = s.prop1 + (s.two ? s.prop2 : 0);
    var dryTotal = s.dry1 + (s.two ? s.dry2 : 0);
    return { stages: stages, total: total, liftoff: liftoff, prop: propTotal, dry: dryTotal };
  }

  /* ---------- build the static bits ---------- */
  fillSelect($("eng1")); fillSelect($("eng2"));
  $("scaleMax").textContent = nf0.format(SCALE) + " m/s";

  (function buildScenarios() {
    var host = $("scenarios"), html = "";
    for (var i = 0; i < SCENARIOS.length; i++) {
      html += '<button type="button" class="btn" data-sc="' + SCENARIOS[i].id +
              '" aria-pressed="false">' + SCENARIOS[i].label + "</button>";
    }
    host.innerHTML = html;
    host.addEventListener("click", function (e) {
      var b = e.target && e.target.closest && e.target.closest("button[data-sc]");
      if (!b) return;
      for (var i = 0; i < SCENARIOS.length; i++) {
        if (SCENARIOS[i].id === b.getAttribute("data-sc")) {
          s = Object.assign({}, SCENARIOS[i].s); syncInputs(); render(true);
        }
      }
    });
  })();

  (function buildMarkLabels() {
    var above = $("labAbove"), below = $("labBelow"), ha = "", hb = "";
    for (var i = 0; i < MARKS.length; i++) {
      var m = MARKS[i], pct = m.dv / SCALE * 100;
      var align = pct > 84 ? "r" : (pct < 6 ? "l" : "c");
      var el = '<span class="lab ' + align + '" style="left:' + pct.toFixed(2) + '%">' +
               '<span class="pin mono" id="bpin' + m.n + '">' + m.n + "</span><span class=\"nm\">" + m.name + "</span></span>";
      if (i % 2 === 0) ha += el; else hb += el;
    }
    above.innerHTML = ha; below.innerHTML = hb;
  })();

  (function buildMarks() {
    var html = "";
    for (var i = 0; i < MARKS.length; i++) {
      var m = MARKS[i];
      html += '<li class="mark" id="mk' + m.n + '">' +
              '<span class="pin mono" aria-hidden="true">' + m.n + "</span>" +
              '<span class="mk-name">' + m.name + "</span>" +
              '<span class="mk-dv">' + nf0.format(m.dv) + " m/s</span>" +
              '<span class="mk-note" id="mkn' + m.n + '">' + m.note + "</span></li>";
    }
    $("marks").innerHTML = html;
  })();

  (function buildTicks() {
    var html = "";
    for (var i = 0; i < MARKS.length; i++) {
      html += '<div class="tick" style="left:calc(' + (MARKS[i].dv / SCALE * 100).toFixed(2) +
              '% - 1px)"></div>';
    }
    $("track").insertAdjacentHTML("beforeend", html);
  })();

  /* ---------- render ---------- */
  function render(announce) {
    var r = calc(), dv = r.total;

    /* big readout */
    $("dvBig").innerHTML = nf0.format(Math.round(dv)) + '<span class="u">m/s</span>';

    /* which mileposts are cleared */
    var top = null;
    for (var i = 0; i < MARKS.length; i++) if (dv >= MARKS[i].dv) top = MARKS[i];
    $("hero").classList.toggle("ok", !!top);

    for (var j = 0; j < MARKS.length; j++) {
      var m = MARKS[j], on = dv >= m.dv, li = $("mk" + m.n), bp = $("bpin" + m.n);
      li.classList.toggle("on", on);
      if (bp) bp.classList.toggle("on", on);
      var note = $("mkn" + m.n);
      if (on) note.innerHTML = "cleared with <b>" + ms(dv - m.dv) + "</b> to spare — " + m.note;
      else note.innerHTML = "needs <b>" + ms(m.dv - dv) + "</b> more — " + m.note;
    }

    /* bar */
    var fillPct = Math.min(dv, SCALE) / SCALE * 100;
    var earnPct = top ? top.dv / SCALE * 100 : 0;
    $("fill").style.width = fillPct.toFixed(2) + "%";
    $("earned").style.width = earnPct.toFixed(2) + "%";
    $("track").setAttribute("aria-label",
      "Budget bar: " + ms(dv) + " out of a " + nf0.format(SCALE) + " m/s scale. " +
      (top ? "Highest milepost cleared: " + top.name + "." : "No milepost cleared yet."));
    var over = $("over");
    if (dv > SCALE) { over.hidden = false; over.textContent = "+" + ms(dv - SCALE) + " past the end of the chart"; }
    else over.hidden = true;

    /* verdict */
    var v, hint;
    if (!top) {
      v = 'Not enough to reach orbit — short by <span class="n">' + ms(MARKS[0].dv - dv) +
          "</span>. It goes up, and it comes back down.";
    } else if (top.n === 1) {
      var spare = dv - MARKS[0].dv;
      v = spare < 300
        ? '<b>Just barely</b> enough to reach a low orbit — <span class="n">' + ms(spare) +
          "</span> of margin, which a real flight would eat on the way up."
        : 'Comfortably into low orbit, with <span class="n">' + ms(spare) + "</span> to spare.";
    } else if (top.n === 2) {
      v = "Enough to leave low orbit behind and push out to a <b>high orbit</b>.";
    } else if (top.n === 3) {
      v = "Enough to swing <b>around the Moon</b> and come home.";
    } else {
      v = 'Enough to brake into lunar orbit and <b>land</b>, with <span class="n">' +
          ms(dv - MARKS[3].dv) + "</span> left over.";
    }
    $("verdict").innerHTML = v;

    if (s.two) {
      var st2 = r.stages[1];
      hint = 'Stage 2 alone contributes <span class="n">' + ms(st2.dv) + "</span> — " +
             '<span class="n">' + Math.round(st2.dv / dv * 100) + "%</span> of the total from " +
             '<span class="n">' + Math.round((st2.prop + st2.dry) / r.liftoff * 100) +
             "%</span> of the liftoff mass.";
    } else if (!top) {
      hint = "Adding a second stage usually beats adding propellant — try the toggle.";
    } else {
      hint = "Turn on the second stage to see what dropping empty tanks is worth.";
    }
    $("hint").innerHTML = hint;

    /* mass stack */
    var two = s.two;
    var segs = [
      { c: "sg-p1", m: s.prop1, l: two ? "propellant 1" : "propellant" },
      { c: "sg-d1", m: s.dry1, l: two ? "structure 1" : "structure" }
    ];
    if (two) {
      segs.push({ c: "sg-p2", m: s.prop2, l: "propellant 2" });
      segs.push({ c: "sg-d2", m: s.dry2, l: "structure 2" });
    }
    segs.push({ c: "sg-pay", m: s.payload, l: "payload" });
    var sh = "", lh = "";
    for (var k = 0; k < segs.length; k++) {
      var w = segs[k].m / r.liftoff * 100;
      sh += '<span class="' + segs[k].c + '" style="width:' + w.toFixed(2) + '%" title="' +
            segs[k].l + " " + t(segs[k].m) + '"></span>';
      lh += '<li><span class="sw ' + segs[k].c + '"></span>' + segs[k].l + " " + t(segs[k].m) + "</li>";
    }
    $("stack").innerHTML = sh;
    $("legend").innerHTML = lh;
    $("stM0").textContent = t(r.liftoff);
    $("stProp").textContent = (r.prop / r.liftoff * 100).toFixed(1) + "%";
    $("stPay").textContent = t(s.payload);
    $("stFrac").textContent = (s.payload / r.liftoff * 100).toFixed(1) + "%";

    /* equation rows */
    var eh = "";
    for (var q = 0; q < r.stages.length; q++) {
      var st = r.stages[q];
      eh += '<div class="eqrow"><div class="eq-h">' +
            '<span class="eq-t">Stage ' + st.i + " · " + engineName(st.ve) + "</span>" +
            '<span class="eq-r">mass ratio ' + (st.m0 / st.mf).toFixed(2) + "</span></div>" +
            '<p class="eq-m">' + nf0.format(st.ve) + " m/s &times; ln( <b>" + t(st.m0) +
            "</b> &divide; <b>" + t(st.mf) + '</b> ) = <span class="res">' + ms(st.dv) +
            "</span></p></div>";
    }
    if (r.stages.length > 1) {
      eh += '<div class="eqrow"><div class="eq-h"><span class="eq-t">Total</span></div>' +
            '<p class="eq-m">' + ms(r.stages[0].dv) + " + " + ms(r.stages[1].dv) +
            ' = <span class="res">' + ms(dv) + "</span></p></div>";
    }
    $("eqRows").innerHTML = eh;

    if (announce) {
      $("live").textContent = ms(dv) + " total. " +
        (top ? "Reaches " + top.name + "." : "Does not reach orbit.");
    }

    /* scenario buttons reflect an exact match */
    var btns = $("scenarios").querySelectorAll("button[data-sc]");
    for (var b = 0; b < btns.length; b++) {
      var sc = null;
      for (var z = 0; z < SCENARIOS.length; z++) {
        if (SCENARIOS[z].id === btns[b].getAttribute("data-sc")) sc = SCENARIOS[z];
      }
      var same = !!sc;
      if (sc) for (var key in sc.s) if (sc.s[key] !== s[key]) same = false;
      btns[b].classList.toggle("active", same);
      btns[b].setAttribute("aria-pressed", same ? "true" : "false");
    }
  }

  /* ---------- inputs ---------- */
  function syncInputs() {
    IN.payload.value = s.payload; IN.ve1.value = s.ve1; IN.prop1.value = s.prop1;
    IN.dry1.value = s.dry1; IN.ve2.value = s.ve2; IN.prop2.value = s.prop2; IN.dry2.value = s.dry2;
    $("two").checked = s.two;
    $("s2fs").disabled = !s.two;
    syncOutputs();
  }

  function syncOutputs() {
    $("oPayload").textContent = t(s.payload);
    $("oVe1").textContent = nf0.format(s.ve1) + " m/s";
    $("oProp1").textContent = t(s.prop1);
    $("oDry1").textContent = t(s.dry1);
    $("oVe2").textContent = nf0.format(s.ve2) + " m/s";
    $("oProp2").textContent = t(s.prop2);
    $("oDry2").textContent = t(s.dry2);
    IN.payload.setAttribute("aria-valuetext", t(s.payload));
    IN.prop1.setAttribute("aria-valuetext", t(s.prop1));
    IN.dry1.setAttribute("aria-valuetext", t(s.dry1));
    IN.prop2.setAttribute("aria-valuetext", t(s.prop2));
    IN.dry2.setAttribute("aria-valuetext", t(s.dry2));
    IN.ve1.setAttribute("aria-valuetext", nf0.format(s.ve1) + " metres per second");
    IN.ve2.setAttribute("aria-valuetext", nf0.format(s.ve2) + " metres per second");
    $("sIsp1").textContent = "≈ " + Math.round(s.ve1 / G0) + " s specific impulse";
    $("sIsp2").textContent = "≈ " + Math.round(s.ve2 / G0) + " s specific impulse";
    setSelect($("eng1"), s.ve1);
    setSelect($("eng2"), s.ve2);
  }

  function setSelect(sel, ve) {
    var found = false;
    for (var i = 0; i < ENGINES.length; i++) if (ENGINES[i].ve === ve) found = true;
    sel.value = found ? String(ve) : "custom";
  }

  function bindRange(el, key) {
    el.addEventListener("input", function () {
      s[key] = parseFloat(el.value);
      syncOutputs(); render(false);
    });
    el.addEventListener("change", function () { render(true); });
  }
  bindRange(IN.payload, "payload");
  bindRange(IN.ve1, "ve1"); bindRange(IN.prop1, "prop1"); bindRange(IN.dry1, "dry1");
  bindRange(IN.ve2, "ve2"); bindRange(IN.prop2, "prop2"); bindRange(IN.dry2, "dry2");

  function bindSelect(sel, key, range) {
    sel.addEventListener("change", function () {
      if (sel.value === "custom") { setSelect(sel, s[key]); return; }
      s[key] = parseFloat(sel.value);
      range.value = s[key];
      syncOutputs(); render(true);
    });
  }
  bindSelect($("eng1"), "ve1", IN.ve1);
  bindSelect($("eng2"), "ve2", IN.ve2);

  $("two").addEventListener("change", function () {
    s.two = $("two").checked;
    $("s2fs").disabled = !s.two;
    render(true);
  });

  $("controls").addEventListener("submit", function (e) { e.preventDefault(); });

  $("reset").addEventListener("click", function () {
    s = Object.assign({}, DEFAULTS); syncInputs(); render(true);
  });

  syncInputs();
  render(false);
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong