Created
March 15, 2026 08:15
-
-
Save dickinsonre/18ad5ab32d5125fde8438eee4433d672 to your computer and use it in GitHub Desktop.
SWMM5 Complete Runoff Explorer — Three Surfaces · Cash-Karp RK5 · ICM InfoWorks Mapping
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SWMM5 Complete Runoff Engine — Three Surfaces · Cash-Karp RK5 · Design Storms</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap'); | |
| :root { | |
| --bg: #080c16; | |
| --bg2: #0f1525; | |
| --bg3: #162036; | |
| --bg4: #1e2d4a; | |
| --panel: #101a2c; | |
| --border: #243050; | |
| --accent: #38bdf8; | |
| --accent2: #818cf8; | |
| --green: #34d399; | |
| --gold: #fbbf24; | |
| --orange: #fb923c; | |
| --red: #f87171; | |
| --txt: #e2e8f0; | |
| --txt2: #94a3b8; | |
| --txt3: #5a7090; | |
| --imperv1: #546E7A; | |
| --imperv2: #78909C; | |
| --perv: #4E7A3E; | |
| --water: #2196F3; | |
| --code-bg: #070a12; | |
| } | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { font-family:'Outfit',sans-serif; background:var(--bg); color:var(--txt); min-height:100vh; } | |
| /* ===== HEADER ===== */ | |
| .hdr { | |
| background: linear-gradient(135deg, #080c16 0%, #1a1545 40%, #0d2040 100%); | |
| padding: 32px 24px 22px; | |
| text-align: center; | |
| border-bottom: 1px solid var(--border); | |
| position: relative; | |
| } | |
| .hdr::after { content:''; position:absolute; bottom:0; left:15%; right:15%; height:1px; | |
| background:linear-gradient(90deg,transparent,var(--accent),var(--green),transparent); } | |
| .hdr h1 { font-size:clamp(1.4rem,3.5vw,2.2rem); font-weight:800; letter-spacing:-0.5px; | |
| background:linear-gradient(135deg,var(--accent),var(--accent2),var(--green)); | |
| -webkit-background-clip:text; -webkit-text-fill-color:transparent; } | |
| .hdr p { color:var(--txt2); font-size:0.88rem; margin-top:6px; font-weight:300; } | |
| .hdr .tags { margin-top:8px; display:flex; gap:8px; justify-content:center; flex-wrap:wrap; } | |
| .hdr .tag { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; padding:3px 10px; | |
| border-radius:4px; border:1px solid rgba(255,255,255,0.1); } | |
| .tag-ode { color:var(--green); background:rgba(52,211,153,0.08); border-color:rgba(52,211,153,0.2)!important; } | |
| .tag-surf { color:var(--accent); background:rgba(56,189,248,0.08); border-color:rgba(56,189,248,0.2)!important; } | |
| .tag-rain { color:var(--gold); background:rgba(251,191,36,0.08); border-color:rgba(251,191,36,0.2)!important; } | |
| .wrap { max-width:1500px; margin:0 auto; padding:16px; } | |
| /* ===== TABS ===== */ | |
| .tabs { display:flex; gap:3px; background:var(--bg2); border-radius:10px; padding:3px; | |
| margin-bottom:18px; border:1px solid var(--border); overflow-x:auto; } | |
| .tbtn { flex:1; min-width:100px; padding:9px 12px; border:none; background:transparent; | |
| color:var(--txt3); font-family:'Outfit',sans-serif; font-size:0.82rem; font-weight:500; | |
| cursor:pointer; border-radius:7px; transition:all 0.2s; white-space:nowrap; } | |
| .tbtn:hover { color:var(--txt); background:var(--bg3); } | |
| .tbtn.on { background:var(--bg4); color:var(--accent); box-shadow:0 2px 8px rgba(56,189,248,0.12); } | |
| .tpanel { display:none; } | |
| .tpanel.on { display:block; } | |
| /* ===== SHARED COMPONENTS ===== */ | |
| .card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; } | |
| .card h3 { font-size:0.9rem; font-weight:600; color:var(--accent); margin-bottom:10px; } | |
| .card h4 { font-size:0.82rem; font-weight:500; color:var(--txt2); margin-bottom:8px; } | |
| .sep { border:none; border-top:1px solid var(--border); margin:12px 0; } | |
| .grid2 { display:grid; grid-template-columns:1fr 1fr; gap:14px; } | |
| .grid3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:14px; } | |
| @media(max-width:900px){ .grid2,.grid3{ grid-template-columns:1fr; } } | |
| .ctrl { margin-bottom:8px; } | |
| .ctrl label { display:flex; justify-content:space-between; font-size:0.76rem; color:var(--txt2); margin-bottom:3px; } | |
| .ctrl .v { font-family:'IBM Plex Mono',monospace; font-weight:500; color:var(--accent); font-size:0.74rem; } | |
| .ctrl input[type="range"] { width:100%; -webkit-appearance:none; height:5px; border-radius:3px; | |
| background:var(--bg4); outline:none; } | |
| .ctrl input[type="range"]::-webkit-slider-thumb { -webkit-appearance:none; width:14px; height:14px; | |
| border-radius:50%; background:var(--accent); cursor:pointer; box-shadow:0 0 6px rgba(56,189,248,0.35); } | |
| .ctrl select { width:100%; background:var(--bg3); border:1px solid var(--border); border-radius:5px; | |
| color:var(--txt); padding:6px 8px; font-family:'Outfit',sans-serif; font-size:0.78rem; } | |
| .stitle { font-size:0.68rem; font-weight:600; color:var(--txt3); text-transform:uppercase; | |
| letter-spacing:1.2px; margin:12px 0 6px; padding-top:10px; border-top:1px solid var(--border); } | |
| .stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(115px,1fr)); gap:8px; } | |
| .st { background:var(--bg3); border-radius:7px; padding:8px 10px; text-align:center; } | |
| .st .sv { font-family:'IBM Plex Mono',monospace; font-size:1.05rem; font-weight:600; } | |
| .st .sl { font-size:0.66rem; color:var(--txt3); margin-top:2px; } | |
| .btn { padding:10px; border:none; border-radius:7px; font-family:'Outfit',sans-serif; font-weight:600; | |
| font-size:0.85rem; cursor:pointer; transition:all 0.2s; width:100%; } | |
| .btn-go { background:linear-gradient(135deg,#0097A7,#00838F); color:#fff; } | |
| .btn-go:hover { box-shadow:0 4px 16px rgba(0,151,167,0.35); transform:translateY(-1px); } | |
| .btn-stop { background:var(--red); color:#fff; } | |
| .eq { background:var(--code-bg); border:1px solid var(--border); border-radius:7px; padding:13px; | |
| font-family:'IBM Plex Mono',monospace; font-size:0.7rem; line-height:1.65; color:var(--txt2); | |
| overflow-x:auto; white-space:pre; } | |
| .code { background:var(--code-bg); border:1px solid var(--border); border-radius:7px; padding:12px; | |
| font-family:'IBM Plex Mono',monospace; font-size:0.68rem; line-height:1.6; overflow-x:auto; | |
| color:var(--txt2); white-space:pre; } | |
| .kw{color:#c084fc} .fn{color:#38bdf8} .cm{color:#4ade80;font-style:italic} .num{color:#fbbf24} .tp{color:#818cf8} | |
| /* ===== ARCH CARDS ===== */ | |
| .agrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:12px; } | |
| .acard { background:var(--bg2); border:1px solid var(--border); border-radius:10px; padding:14px; | |
| cursor:pointer; transition:all 0.25s; position:relative; overflow:hidden; } | |
| .acard::before { content:''; position:absolute; top:0; left:0; right:0; height:2px; | |
| background:linear-gradient(90deg,var(--accent),var(--accent2)); opacity:0; transition:0.25s; } | |
| .acard:hover { border-color:var(--accent); transform:translateY(-2px); } | |
| .acard:hover::before,.acard.exp::before { opacity:1; } | |
| .acard .ahead { display:flex; align-items:center; gap:8px; margin-bottom:6px; } | |
| .acard .aico { width:32px; height:32px; border-radius:6px; display:flex; align-items:center; | |
| justify-content:center; font-size:1rem; flex-shrink:0; } | |
| .acard .atitle { font-family:'IBM Plex Mono',monospace; font-size:0.82rem; font-weight:600; } | |
| .acard .afile { font-size:0.68rem; color:var(--txt3); font-family:'IBM Plex Mono',monospace; } | |
| .acard .adesc { font-size:0.78rem; color:var(--txt2); line-height:1.45; } | |
| .acard .adetail { display:none; margin-top:12px; padding-top:10px; border-top:1px solid var(--border); } | |
| .acard.exp .adetail { display:block; } | |
| .ico-h { background:rgba(56,189,248,0.12); color:var(--accent); } | |
| .ico-o { background:rgba(52,211,153,0.12); color:var(--green); } | |
| .ico-i { background:rgba(251,191,36,0.12); color:var(--gold); } | |
| .ico-r { background:rgba(129,140,248,0.12); color:var(--accent2); } | |
| .ico-c { background:rgba(248,113,113,0.12); color:var(--red); } | |
| /* ===== RK STAGES ===== */ | |
| .rkrow { display:grid; grid-template-columns:repeat(6,1fr); gap:6px; } | |
| @media(max-width:700px){ .rkrow{ grid-template-columns:repeat(3,1fr); } } | |
| .rkbox { background:var(--bg3); border:1px solid var(--border); border-radius:7px; padding:8px; | |
| text-align:center; transition:all 0.35s; } | |
| .rkbox.lit { border-color:var(--green); background:rgba(52,211,153,0.06); | |
| box-shadow:0 0 10px rgba(52,211,153,0.12); } | |
| .rkbox .rn { font-family:'IBM Plex Mono',monospace; font-size:0.78rem; font-weight:600; color:var(--green); } | |
| .rkbox .rv { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; color:var(--txt2); margin-top:3px; } | |
| /* ===== VISUAL CANVAS ===== */ | |
| .vizwrap { background:var(--panel); border:1px solid var(--border); border-radius:12px; | |
| overflow:hidden; position:relative; margin-bottom:14px; } | |
| .vizwrap canvas { width:100%; display:block; } | |
| .viztime { position:absolute; top:10px; right:14px; font-family:'IBM Plex Mono',monospace; | |
| font-size:0.8rem; color:var(--accent); background:rgba(8,12,22,0.85); padding:3px 9px; | |
| border-radius:4px; border:1px solid var(--border); } | |
| /* ===== SIM LAYOUT ===== */ | |
| .simlayout { display:grid; grid-template-columns:285px 1fr; gap:16px; } | |
| @media(max-width:1000px){ .simlayout{ grid-template-columns:1fr; } } | |
| .simright { display:flex; flex-direction:column; gap:14px; } | |
| /* ===== FLOW SVG ===== */ | |
| svg.flowsvg { width:100%; max-width:1100px; margin:0 auto; display:block; } | |
| /* Legend */ | |
| .legend { display:flex; gap:14px; flex-wrap:wrap; margin-bottom:12px; } | |
| .litem { display:flex; align-items:center; gap:5px; font-size:0.74rem; color:var(--txt2); } | |
| .ldot { width:10px; height:10px; border-radius:50%; } | |
| /* Surface legend */ | |
| .surfleg { display:flex; flex-direction:column; gap:5px; margin:8px 0; } | |
| .sitem { display:flex; align-items:center; gap:7px; font-size:0.73rem; color:var(--txt2); } | |
| .sdot { width:14px; height:9px; border-radius:2px; flex-shrink:0; } | |
| /* RK live panel within simulator */ | |
| .rklive { background:var(--bg2); border:1px solid var(--border); border-radius:10px; padding:14px; } | |
| .rklive h4 { font-size:0.82rem; color:var(--green); margin-bottom:8px; } | |
| footer { text-align:center; padding:26px; color:var(--txt3); font-size:0.72rem; | |
| border-top:1px solid var(--border); margin-top:30px; } | |
| footer a { color:var(--accent); text-decoration:none; } | |
| /* ===== ICM TAB ===== */ | |
| .icm-orange { color: #fb923c; } | |
| .swmm-blue { color: #38bdf8; } | |
| .mapping-grid { display:grid; grid-template-columns:1fr 40px 1fr; gap:0; margin:14px 0; } | |
| .map-col { background:var(--bg2); border:1px solid var(--border); border-radius:0; padding:14px; } | |
| .map-col:first-child { border-radius:10px 0 0 10px; } | |
| .map-col:last-child { border-radius:0 10px 10px 0; } | |
| .map-arrow-col { display:flex; align-items:center; justify-content:center; background:var(--bg3); | |
| border-top:1px solid var(--border); border-bottom:1px solid var(--border); font-size:1.2rem; color:var(--green); } | |
| .map-col h4 { font-size:0.82rem; font-weight:700; margin-bottom:10px; text-align:center; } | |
| .map-row { display:flex; justify-content:space-between; align-items:center; padding:5px 8px; | |
| border-radius:5px; margin-bottom:3px; font-size:0.74rem; } | |
| .map-row:nth-child(even) { background:rgba(255,255,255,0.02); } | |
| .map-row .mf { font-family:'IBM Plex Mono',monospace; font-weight:500; } | |
| .map-row .ml { color:var(--txt3); font-size:0.68rem; } | |
| .icm-steps { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:12px; margin:14px 0; } | |
| .icm-step { background:var(--bg2); border:1px solid var(--border); border-radius:10px; padding:14px; | |
| position:relative; } | |
| .icm-step .step-num { position:absolute; top:-10px; left:14px; background:var(--orange); | |
| color:#000; font-weight:700; font-size:0.72rem; width:22px; height:22px; border-radius:50%; | |
| display:flex; align-items:center; justify-content:center; } | |
| .icm-step h5 { font-size:0.82rem; color:var(--orange); margin-bottom:6px; margin-top:4px; } | |
| .icm-step p { font-size:0.74rem; color:var(--txt2); line-height:1.5; } | |
| .param-table { width:100%; border-collapse:collapse; font-size:0.72rem; margin:10px 0; } | |
| .param-table th { background:var(--bg4); color:var(--txt); padding:7px 8px; text-align:left; | |
| font-weight:600; border:1px solid var(--border); } | |
| .param-table td { padding:6px 8px; border:1px solid var(--border); color:var(--txt2); } | |
| .param-table td.swmm { color:var(--accent); font-family:'IBM Plex Mono',monospace; font-weight:500; } | |
| .param-table td.icm { color:var(--orange); font-family:'IBM Plex Mono',monospace; font-weight:500; } | |
| .param-table tr:nth-child(even) { background:rgba(255,255,255,0.015); } | |
| .icm-note { background:rgba(251,191,36,0.06); border:1px solid rgba(251,191,36,0.2); | |
| border-radius:8px; padding:12px 14px; margin:10px 0; font-size:0.76rem; color:var(--gold); line-height:1.5; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="hdr"> | |
| <h1>SWMM5 Runoff Engine — Complete Explorer</h1> | |
| <p>Three Runoff Surfaces · Cash-Karp RK5 ODE Solver · Animated Subcatchment · Design Storms</p> | |
| <div class="tags"> | |
| <span class="tag tag-surf">subcatch.c — 3 surfaces</span> | |
| <span class="tag tag-ode">odesolve.c — Cash-Karp RK5</span> | |
| <span class="tag tag-rain">runoff.c · infil.c · gage.c</span> | |
| </div> | |
| </div> | |
| <div class="wrap"> | |
| <div class="tabs"> | |
| <button class="tbtn on" data-t="arch">🏗️ Code Map</button> | |
| <button class="tbtn" data-t="flow">🔀 Call Flow</button> | |
| <button class="tbtn" data-t="rk">🧮 RK5 Deep-Dive</button> | |
| <button class="tbtn" data-t="sim">🌧️ 3-Surface Simulator</button> | |
| <button class="tbtn" data-t="icm">🔗 ICM InfoWorks ↔ SWMM5</button> | |
| </div> | |
| <!-- ==================== TAB 1: ARCHITECTURE ==================== --> | |
| <div id="t-arch" class="tpanel on"> | |
| <div class="legend"> | |
| <div class="litem"><div class="ldot" style="background:var(--accent)"></div>Hydrology/Runoff</div> | |
| <div class="litem"><div class="ldot" style="background:var(--gold)"></div>Infiltration</div> | |
| <div class="litem"><div class="ldot" style="background:var(--green)"></div>ODE Solver</div> | |
| <div class="litem"><div class="ldot" style="background:var(--accent2)"></div>Routing</div> | |
| <div class="litem"><div class="ldot" style="background:var(--red)"></div>Core Engine</div> | |
| </div> | |
| <div class="agrid" id="archGrid"></div> | |
| </div> | |
| <!-- ==================== TAB 2: CALL FLOW ==================== --> | |
| <div id="t-flow" class="tpanel"> | |
| <div class="card" style="margin-bottom:16px;"> | |
| <h3>SWMM5 Runoff Computation Call Graph</h3> | |
| <svg class="flowsvg" viewBox="0 0 1100 680" id="flowSvg"></svg> | |
| </div> | |
| <div class="eq"> | |
| <b style="color:#38bdf8">The Nonlinear Reservoir — Three Surfaces (subcatch.c)</b> | |
| SWMM5 splits each subcatchment into <b style="color:#546E7A">A1</b>, <b style="color:#78909C">A2</b>, <b style="color:#4E7A3E">A3</b>: | |
| <b style="color:#546E7A">A1</b> Imperv (no DS): dd₁/dt = i(t) − q₁(d₁) d_s = 0 | |
| <b style="color:#78909C">A2</b> Imperv (w/ DS): dd₂/dt = i(t) − e(t) − q₂(d₂) d_s > 0 | |
| <b style="color:#4E7A3E">A3</b> Pervious: dd₃/dt = i(t) − e(t) − f(t) − q₃(d₃) d_s > 0, infiltration | |
| Outflow: qⱼ = Wⱼ · (1.49/nⱼ) · (dⱼ − d_sⱼ)^(5/3) · S^(1/2) [Manning's overland flow] | |
| Total Q = q₁ + q₂ + q₃ | |
| <b style="color:#34d399">ODE Solution (odesolve.c):</b> Each surface depth solved independently via Cash-Karp RK5. | |
| 6 function evaluations per step, embedded 4th/5th order error estimate, adaptive step-size. | |
| </div> | |
| </div> | |
| <!-- ==================== TAB 3: RK5 DEEP-DIVE ==================== --> | |
| <div id="t-rk" class="tpanel"> | |
| <div class="card" style="margin-bottom:14px;"> | |
| <h3>Cash-Karp RK5 Adaptive Solver — odesolve.c</h3> | |
| <p style="font-size:0.82rem;color:var(--txt2);margin-bottom:14px;"> | |
| SWMM5 solves each surface's depth ODE with a 5th-order Runge-Kutta (Cash-Karp variant). | |
| Six stages k₁…k₆ are evaluated per step. The difference between 5th and 4th order solutions | |
| gives a truncation error estimate for adaptive step-size control. | |
| </p> | |
| <div class="rkrow" id="rkStages"> | |
| <div class="rkbox" id="rk-k1"><div class="rn">k₁</div><div class="rv" id="rv1">—</div></div> | |
| <div class="rkbox" id="rk-k2"><div class="rn">k₂</div><div class="rv" id="rv2">—</div></div> | |
| <div class="rkbox" id="rk-k3"><div class="rn">k₃</div><div class="rv" id="rv3">—</div></div> | |
| <div class="rkbox" id="rk-k4"><div class="rn">k₄</div><div class="rv" id="rv4">—</div></div> | |
| <div class="rkbox" id="rk-k5"><div class="rn">k₅</div><div class="rv" id="rv5">—</div></div> | |
| <div class="rkbox" id="rk-k6"><div class="rn">k₆</div><div class="rv" id="rv6">—</div></div> | |
| </div> | |
| </div> | |
| <div class="grid2"> | |
| <div class="eq" style="font-size:0.67rem;"> | |
| <b style="color:#c084fc">Cash-Karp Butcher Tableau (odesolve.c)</b> | |
| 0 | | |
| 1/5 | 1/5 | |
| 3/10 | 3/40 9/40 | |
| 3/5 | 3/10 −9/10 6/5 | |
| 1 |−11/54 5/2 −70/27 35/27 | |
| 7/8 | 1631/55296 175/512 575/13824 44275/110592 253/4096 | |
| ──────────────────────────────────────────────────────────── | |
| 5th: 37/378 0 250/621 125/594 0 512/1771 | |
| 4th: 2825/27648 0 18575/48384 13525/55296 277/14336 1/4 | |
| </div> | |
| <div class="eq" style="font-size:0.67rem;"> | |
| <b style="color:#38bdf8">Step-Size Control (odesolve.c)</b> | |
| <span class="cm">// Truncation error estimate</span> | |
| err = max |y₅[i] − y₄[i]| / (|y₅[i]| + TINY) | |
| <span class="kw">if</span> (err > TOL) { | |
| h_new = SAFETY · h · err^(−0.25) <span class="cm">// shrink</span> | |
| h_new = max(h_new, 0.1·h) <span class="cm">// floor</span> | |
| } <span class="kw">else</span> { | |
| h_new = SAFETY · h · err^(−0.2) <span class="cm">// grow</span> | |
| h_new = min(h_new, 5.0·h) <span class="cm">// ceiling</span> | |
| } | |
| SAFETY = 0.9, TINY = 1.0e-30, TOL = 1.0e-5 | |
| </div> | |
| </div> | |
| <div class="card" style="margin-top:14px;"> | |
| <h4 style="color:var(--green);">Interactive RK5 Single-Step Explorer</h4> | |
| <div class="grid3" style="margin-bottom:10px;"> | |
| <div class="ctrl"><label>Surface <span class="v" id="rkSurf">A3 Pervious</span></label> | |
| <select id="rkSurfSel" onchange="doRK()"> | |
| <option value="a1">A1 — Imperv (no DS)</option> | |
| <option value="a2">A2 — Imperv (w/ DS)</option> | |
| <option value="a3" selected>A3 — Pervious</option> | |
| </select></div> | |
| <div class="ctrl"><label>Depth d₀ (ft) <span class="v" id="rkD0v">0.010</span></label> | |
| <input type="range" id="rkD0" min="0.001" max="0.1" step="0.001" value="0.01" oninput="doRK()"></div> | |
| <div class="ctrl"><label>Rain (in/hr) <span class="v" id="rkRv">2.0</span></label> | |
| <input type="range" id="rkR" min="0" max="10" step="0.1" value="2" oninput="doRK()"></div> | |
| </div> | |
| <div class="grid3" style="margin-bottom:10px;"> | |
| <div class="ctrl"><label>Δt (sec) <span class="v" id="rkDTv">30</span></label> | |
| <input type="range" id="rkDT" min="1" max="600" step="1" value="30" oninput="doRK()"></div> | |
| <div class="ctrl"><label>Manning n <span class="v" id="rkNv">0.015</span></label> | |
| <input type="range" id="rkN" min="0.01" max="0.5" step="0.001" value="0.015" oninput="doRK()"></div> | |
| <div class="ctrl"><label>Dep. Storage (in) <span class="v" id="rkDSv">0.05</span></label> | |
| <input type="range" id="rkDS" min="0" max="0.5" step="0.005" value="0.05" oninput="doRK()"></div> | |
| </div> | |
| <div class="stats" id="rkStats"></div> | |
| </div> | |
| </div> | |
| <!-- ==================== TAB 4: 3-SURFACE SIMULATOR ==================== --> | |
| <div id="t-sim" class="tpanel"> | |
| <div class="simlayout"> | |
| <!-- Controls --> | |
| <div> | |
| <div class="card"> | |
| <h3>🌧️ Design Storm</h3> | |
| <div class="ctrl"><label>Pattern</label> | |
| <select id="sType" onchange="resetSim()"> | |
| <option value="scs2">SCS Type II (24-hr)</option> | |
| <option value="scs1">SCS Type I</option> | |
| <option value="scs1a">SCS Type IA</option> | |
| <option value="scs3">SCS Type III</option> | |
| <option value="chicago">Chicago (Keifer-Chu)</option> | |
| <option value="uniform">Uniform Block</option> | |
| <option value="triangular">Triangular</option> | |
| </select></div> | |
| <div class="ctrl"><label>Total Depth <span class="v" id="vP">4.0 in</span></label> | |
| <input type="range" id="sP" min="0.5" max="12" step="0.1" value="4" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>Duration <span class="v" id="vD">24 hr</span></label> | |
| <input type="range" id="sD" min="1" max="72" step="1" value="24" oninput="resetSim()"></div> | |
| <div class="stitle">Subcatchment</div> | |
| <div class="ctrl"><label>Area <span class="v" id="vA">10 ac</span></label> | |
| <input type="range" id="sA" min="1" max="200" step="1" value="10" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>Slope <span class="v" id="vSl">2.0%</span></label> | |
| <input type="range" id="sSl" min="0.1" max="12" step="0.1" value="2" oninput="resetSim()"></div> | |
| <div class="stitle">Three Surfaces</div> | |
| <div class="surfleg"> | |
| <div class="sitem"><div class="sdot" style="background:var(--imperv1)"></div><b>A1</b> Imperv no DS</div> | |
| <div class="sitem"><div class="sdot" style="background:var(--imperv2)"></div><b>A2</b> Imperv w/ DS</div> | |
| <div class="sitem"><div class="sdot" style="background:var(--perv)"></div><b>A3</b> Pervious</div> | |
| </div> | |
| <div class="ctrl"><label>% Impervious <span class="v" id="vImp">50%</span></label> | |
| <input type="range" id="sImp" min="0" max="100" step="1" value="50" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>% Imperv w/o DS <span class="v" id="vNds">25%</span></label> | |
| <input type="range" id="sNds" min="0" max="100" step="1" value="25" oninput="resetSim()"></div> | |
| <div class="stitle">A1: Imperv (no DS)</div> | |
| <div class="ctrl"><label>n₁ <span class="v" id="vN1">0.012</span></label> | |
| <input type="range" id="sN1" min="0.01" max="0.05" step="0.001" value="0.012" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>Width₁ <span class="v" id="vW1">500 ft</span></label> | |
| <input type="range" id="sW1" min="50" max="3000" step="10" value="500" oninput="resetSim()"></div> | |
| <div class="stitle">A2: Imperv (w/ DS)</div> | |
| <div class="ctrl"><label>n₂ <span class="v" id="vN2">0.012</span></label> | |
| <input type="range" id="sN2" min="0.01" max="0.05" step="0.001" value="0.012" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>d_s₂ <span class="v" id="vDS2">0.05 in</span></label> | |
| <input type="range" id="sDS2" min="0" max="0.3" step="0.005" value="0.05" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>Width₂ <span class="v" id="vW2">500 ft</span></label> | |
| <input type="range" id="sW2" min="50" max="3000" step="10" value="500" oninput="resetSim()"></div> | |
| <div class="stitle">A3: Pervious</div> | |
| <div class="ctrl"><label>n₃ <span class="v" id="vN3">0.150</span></label> | |
| <input type="range" id="sN3" min="0.01" max="0.8" step="0.005" value="0.15" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>d_s₃ <span class="v" id="vDS3">0.20 in</span></label> | |
| <input type="range" id="sDS3" min="0" max="1" step="0.01" value="0.2" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>Width₃ <span class="v" id="vW3">500 ft</span></label> | |
| <input type="range" id="sW3" min="50" max="3000" step="10" value="500" oninput="resetSim()"></div> | |
| <div class="stitle">Horton Infiltration (A3)</div> | |
| <div class="ctrl"><label>f₀ <span class="v" id="vF0">3.0 in/hr</span></label> | |
| <input type="range" id="sF0" min="0.1" max="10" step="0.1" value="3" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>f∞ <span class="v" id="vFc">0.50 in/hr</span></label> | |
| <input type="range" id="sFc" min="0.01" max="4" step="0.01" value="0.5" oninput="resetSim()"></div> | |
| <div class="ctrl"><label>k <span class="v" id="vKd">4.0 /hr</span></label> | |
| <input type="range" id="sKd" min="0.1" max="15" step="0.1" value="4" oninput="resetSim()"></div> | |
| <button class="btn btn-go" id="btnPlay" onclick="toggleAnim()">▶ Run Animation</button> | |
| </div> | |
| </div> | |
| <!-- Right content --> | |
| <div class="simright"> | |
| <!-- Animated cross-section --> | |
| <div class="vizwrap"> | |
| <canvas id="cviz" height="340"></canvas> | |
| <div class="viztime" id="vizTime">t = 0.0 hr</div> | |
| </div> | |
| <!-- Live RK5 stages for current step --> | |
| <div class="rklive"> | |
| <h4>🧮 Live RK5 Stages — Current Time Step (all 3 surfaces)</h4> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;"> | |
| <div> | |
| <div style="font-size:0.72rem;color:var(--imperv1);font-weight:600;margin-bottom:4px;">A1 Imperv (no DS)</div> | |
| <div class="rkrow" id="rkLive1" style="grid-template-columns:repeat(6,1fr);gap:3px;"></div> | |
| </div> | |
| <div> | |
| <div style="font-size:0.72rem;color:var(--imperv2);font-weight:600;margin-bottom:4px;">A2 Imperv (w/ DS)</div> | |
| <div class="rkrow" id="rkLive2" style="grid-template-columns:repeat(6,1fr);gap:3px;"></div> | |
| </div> | |
| <div> | |
| <div style="font-size:0.72rem;color:var(--perv);font-weight:600;margin-bottom:4px;">A3 Pervious</div> | |
| <div class="rkrow" id="rkLive3" style="grid-template-columns:repeat(6,1fr);gap:3px;"></div> | |
| </div> | |
| </div> | |
| <div class="stats" style="margin-top:8px;" id="rkLiveStats"></div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="stats" id="simStats"></div> | |
| <!-- Charts --> | |
| <div class="grid2"> | |
| <div class="card"><h4>Hyetograph & Total Hydrograph</h4><canvas id="c1" height="210"></canvas></div> | |
| <div class="card"><h4>Three-Surface Outflow Breakdown</h4><canvas id="c2" height="210"></canvas></div> | |
| </div> | |
| <div class="grid2"> | |
| <div class="card"><h4>Surface Water Depths (d₁, d₂, d₃)</h4><canvas id="c3" height="195"></canvas></div> | |
| <div class="card"><h4>Infiltration & RK5 Sub-Steps</h4><canvas id="c4" height="195"></canvas></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ==================== TAB 5: ICM INFOWORKS ↔ SWMM5 ==================== --> | |
| <div id="t-icm" class="tpanel"> | |
| <div class="card" style="margin-bottom:14px;"> | |
| <h3 style="color:var(--orange);">🔗 Running SWMM Hydrology in ICM InfoWorks</h3> | |
| <p style="font-size:0.84rem;color:var(--txt2);line-height:1.6;"> | |
| InfoWorks ICM can run <b class="swmm-blue">EPA SWMM runoff</b> natively within an | |
| <b class="icm-orange">InfoWorks network</b>. The same three-surface nonlinear reservoir model — | |
| impervious without depression storage, impervious with depression storage, and pervious with | |
| infiltration — is available as a routing model type called <b class="icm-orange">"SWMM"</b> in the | |
| ICM Runoff Surface editor. This page maps every SWMM5 parameter to its ICM InfoWorks equivalent. | |
| </p> | |
| </div> | |
| <!-- How-to Steps --> | |
| <div class="card" style="margin-bottom:14px;"> | |
| <h3 style="color:var(--orange);">Step-by-Step: Setting Up SWMM Runoff in ICM InfoWorks</h3> | |
| <div class="icm-steps"> | |
| <div class="icm-step"> | |
| <div class="step-num">1</div> | |
| <h5>Create 3 Runoff Surfaces</h5> | |
| <p>In the <b>Runoff Surface</b> editor, create three surfaces:<br> | |
| • <b>Imperv_noDS</b> — Fixed runoff (coeff=1.0), Routing=SWMM<br> | |
| • <b>Imperv_wDS</b> — Fixed runoff (coeff=1.0), Routing=SWMM<br> | |
| • <b>Pervious</b> — Horton or Green-Ampt volume model, Routing=SWMM<br> | |
| Set Manning's n in the <b>Runoff routing value</b> field for each.</p> | |
| </div> | |
| <div class="icm-step"> | |
| <div class="step-num">2</div> | |
| <h5>Create a Land Use</h5> | |
| <p>In the <b>Land Use</b> editor, create a land use that references all three runoff surfaces. | |
| Set the <b>area percentages</b> for each surface to match your SWMM5 subcatchment's | |
| %Imperv and %Zero-Imperv splits.</p> | |
| </div> | |
| <div class="icm-step"> | |
| <div class="step-num">3</div> | |
| <h5>Assign to Subcatchments</h5> | |
| <p>In the <b>Subcatchment</b> editor, assign the Land Use to each subcatchment. | |
| Set the <b>total area</b>, <b>slope</b>, and <b>subcatchment width</b>. | |
| The width applies to all three surfaces in ICM (same as SWMM5).</p> | |
| </div> | |
| <div class="icm-step"> | |
| <div class="step-num">4</div> | |
| <h5>Set Infiltration</h5> | |
| <p>On the pervious Runoff Surface, set the infiltration parameters:<br> | |
| • Horton: f₀ (max rate), f∞ (min rate), k (decay)<br> | |
| • Green-Ampt: Ψ (suction head), Ks (conductivity), Δθ (deficit)<br> | |
| Set initial conditions in the <b>Rainfall Event</b> editor.</p> | |
| </div> | |
| <div class="icm-step"> | |
| <div class="step-num">5</div> | |
| <h5>Configure Rainfall Event</h5> | |
| <p>Add a <b>Rainfall Event</b> with the appropriate profile. | |
| Set initial conditions for Horton moisture store or Green-Ampt moisture deficit. | |
| Match the subcatchment <b>Rainfall Profile</b> name to the event profile name.</p> | |
| </div> | |
| <div class="icm-step"> | |
| <div class="step-num">6</div> | |
| <h5>Run Simulation</h5> | |
| <p>Create a Run with the rainfall event. ICM will use the SWMM nonlinear reservoir + | |
| Manning's overland flow equation (the same RK5 ODE solver) for each of the three surfaces | |
| on every subcatchment.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Parameter Mapping Table --> | |
| <div class="card" style="margin-bottom:14px;"> | |
| <h3>📋 Complete Parameter Mapping: SWMM5 ↔ ICM InfoWorks</h3> | |
| <div class="mapping-grid"> | |
| <div class="map-col"> | |
| <h4 class="swmm-blue">EPA SWMM5 (subcatch.c)</h4> | |
| </div> | |
| <div class="map-arrow-col">↔</div> | |
| <div class="map-col"> | |
| <h4 class="icm-orange">ICM InfoWorks</h4> | |
| </div> | |
| </div> | |
| <h4 style="color:var(--txt2);font-size:0.8rem;margin:14px 0 8px;">Subcatchment-Level Parameters</h4> | |
| <table class="param-table"> | |
| <tr><th>Parameter</th><th>SWMM5 INP Field</th><th>ICM InfoWorks Field</th><th>Editor Location</th></tr> | |
| <tr><td>Total Area</td><td class="swmm">[SUBCATCHMENTS] → Area</td><td class="icm">total_area</td><td>Subcatchment</td></tr> | |
| <tr><td>Outlet Node</td><td class="swmm">[SUBCATCHMENTS] → OutID</td><td class="icm">node_id</td><td>Subcatchment</td></tr> | |
| <tr><td>% Impervious</td><td class="swmm">[SUBCATCHMENTS] → %Imperv</td><td class="icm">Runoff Surface area %</td><td>Land Use → surfaces</td></tr> | |
| <tr><td>Characteristic Width</td><td class="swmm">[SUBCATCHMENTS] → Width</td><td class="icm">width (on subcatchment)</td><td>Subcatchment</td></tr> | |
| <tr><td>Slope</td><td class="swmm">[SUBCATCHMENTS] → %Slope</td><td class="icm">slope</td><td>Subcatchment</td></tr> | |
| <tr><td>Rain Gage</td><td class="swmm">[SUBCATCHMENTS] → Rain Gage</td><td class="icm">rainfall_profile</td><td>Subcatchment</td></tr> | |
| </table> | |
| <h4 style="color:var(--txt2);font-size:0.8rem;margin:14px 0 8px;">Surface A1: Impervious WITHOUT Depression Storage</h4> | |
| <table class="param-table"> | |
| <tr><th>Parameter</th><th>SWMM5 INP Field</th><th>ICM InfoWorks Field</th><th>Editor Location</th></tr> | |
| <tr><td>Manning's n</td><td class="swmm">[SUBAREAS] → N-Imperv</td><td class="icm">runoff_routing_value</td><td>Runoff Surface</td></tr> | |
| <tr><td>Depression Storage</td><td class="swmm">— (d_s = 0 always)</td><td class="icm">initial_loss_value = 0</td><td>Runoff Surface</td></tr> | |
| <tr><td>% of Impervious</td><td class="swmm">[SUBAREAS] → %Zero-Imperv</td><td class="icm">area % in Land Use</td><td>Land Use</td></tr> | |
| <tr><td>Volume Model</td><td class="swmm">Fixed 100% runoff</td><td class="icm">runoff_volume_type = Fixed, coeff = 1.0</td><td>Runoff Surface</td></tr> | |
| <tr><td>Routing Model</td><td class="swmm">Nonlinear reservoir</td><td class="icm">routing_model = SWMM</td><td>Runoff Surface</td></tr> | |
| </table> | |
| <h4 style="color:var(--txt2);font-size:0.8rem;margin:14px 0 8px;">Surface A2: Impervious WITH Depression Storage</h4> | |
| <table class="param-table"> | |
| <tr><th>Parameter</th><th>SWMM5 INP Field</th><th>ICM InfoWorks Field</th><th>Editor Location</th></tr> | |
| <tr><td>Manning's n</td><td class="swmm">[SUBAREAS] → N-Imperv</td><td class="icm">runoff_routing_value</td><td>Runoff Surface</td></tr> | |
| <tr><td>Depression Storage</td><td class="swmm">[SUBAREAS] → S-Imperv</td><td class="icm">initial_loss_value</td><td>Runoff Surface</td></tr> | |
| <tr><td>Initial Loss Type</td><td class="swmm">Depth-based</td><td class="icm">initial_loss_type = Absolute</td><td>Runoff Surface</td></tr> | |
| <tr><td>Volume Model</td><td class="swmm">Fixed 100% runoff</td><td class="icm">runoff_volume_type = Fixed, coeff = 1.0</td><td>Runoff Surface</td></tr> | |
| <tr><td>Routing Model</td><td class="swmm">Nonlinear reservoir</td><td class="icm">routing_model = SWMM</td><td>Runoff Surface</td></tr> | |
| </table> | |
| <h4 style="color:var(--txt2);font-size:0.8rem;margin:14px 0 8px;">Surface A3: Pervious</h4> | |
| <table class="param-table"> | |
| <tr><th>Parameter</th><th>SWMM5 INP Field</th><th>ICM InfoWorks Field</th><th>Editor Location</th></tr> | |
| <tr><td>Manning's n</td><td class="swmm">[SUBAREAS] → N-Perv</td><td class="icm">runoff_routing_value</td><td>Runoff Surface</td></tr> | |
| <tr><td>Depression Storage</td><td class="swmm">[SUBAREAS] → S-Perv</td><td class="icm">initial_loss_value</td><td>Runoff Surface</td></tr> | |
| <tr><td>Volume Model</td><td class="swmm">Horton / Green-Ampt</td><td class="icm">runoff_volume_type = Horton / Green-Ampt</td><td>Runoff Surface</td></tr> | |
| <tr><td>Routing Model</td><td class="swmm">Nonlinear reservoir</td><td class="icm">routing_model = SWMM</td><td>Runoff Surface</td></tr> | |
| </table> | |
| <h4 style="color:var(--txt2);font-size:0.8rem;margin:14px 0 8px;">Horton Infiltration Parameters (Pervious Surface Only)</h4> | |
| <table class="param-table"> | |
| <tr><th>Parameter</th><th>SWMM5 INP Field</th><th>ICM InfoWorks Field</th><th>Editor Location</th></tr> | |
| <tr><td>Max Infil Rate (f₀)</td><td class="swmm">[INFILTRATION] → MaxRate</td><td class="icm">horton_max_rate</td><td>Runoff Surface</td></tr> | |
| <tr><td>Min Infil Rate (f∞)</td><td class="swmm">[INFILTRATION] → MinRate</td><td class="icm">horton_min_rate</td><td>Runoff Surface</td></tr> | |
| <tr><td>Decay Constant (k)</td><td class="swmm">[INFILTRATION] → Decay</td><td class="icm">horton_decay</td><td>Runoff Surface</td></tr> | |
| <tr><td>Drying Time</td><td class="swmm">[INFILTRATION] → DryTime</td><td class="icm">horton_drying_time</td><td>Runoff Surface</td></tr> | |
| <tr><td>Initial Moisture Store</td><td class="swmm">[INFILTRATION] → MaxInfil</td><td class="icm">Set in Rainfall Event → Initial Conditions</td><td>Rainfall Event</td></tr> | |
| </table> | |
| <h4 style="color:var(--txt2);font-size:0.8rem;margin:14px 0 8px;">Green-Ampt Infiltration Parameters (Pervious Surface Only)</h4> | |
| <table class="param-table"> | |
| <tr><th>Parameter</th><th>SWMM5 INP Field</th><th>ICM InfoWorks Field</th><th>Editor Location</th></tr> | |
| <tr><td>Suction Head (Ψ)</td><td class="swmm">[INFILTRATION] → Suction</td><td class="icm">green_ampt_suction</td><td>Runoff Surface</td></tr> | |
| <tr><td>Conductivity (Ks)</td><td class="swmm">[INFILTRATION] → Ksat</td><td class="icm">green_ampt_conductivity</td><td>Runoff Surface</td></tr> | |
| <tr><td>Initial Deficit (Δθ)</td><td class="swmm">[INFILTRATION] → IMD</td><td class="icm">Set in Rainfall Event → Initial Conditions</td><td>Rainfall Event</td></tr> | |
| </table> | |
| </div> | |
| <!-- Key Differences / Notes --> | |
| <div class="card" style="margin-bottom:14px;"> | |
| <h3>⚠️ Key Differences & Notes</h3> | |
| <div class="icm-note"> | |
| <b>Three-Level Hierarchy:</b> In SWMM5, surface parameters live directly on the subcatchment | |
| ([SUBAREAS] + [INFILTRATION] sections). In ICM InfoWorks, there's a three-level hierarchy: | |
| <b>Subcatchment → Land Use → Runoff Surface</b>. Multiple subcatchments can share the same | |
| Land Use, and multiple Land Uses can share the same Runoff Surface definitions. | |
| </div> | |
| <div class="icm-note"> | |
| <b>Width Field:</b> In SWMM5, the width is set once on the subcatchment and applies to all | |
| three surfaces. In ICM InfoWorks, the width is also on the subcatchment, and the SWMM routing | |
| model uses it for all surfaces. The Manning's n is always the <b>Runoff routing value</b> | |
| field regardless of what routing type is displayed in the UI. | |
| </div> | |
| <div class="icm-note"> | |
| <b>Slope-Related vs Absolute Initial Loss:</b> ICM allows both slope-related initial loss | |
| (recommended value 0.000071 m for impervious, matching Wallingford model) and absolute depth. | |
| SWMM5 always uses absolute depth for depression storage. Use <b>Absolute</b> type in ICM to | |
| match SWMM5 behavior exactly. | |
| </div> | |
| <div class="icm-note"> | |
| <b>Infiltration Initial Conditions:</b> SWMM5 puts initial moisture/deficit in the [INFILTRATION] | |
| section. ICM requires these in the <b>Rainfall Event editor → Initial Conditions</b> tab. | |
| This is a common gotcha during conversion — if you miss it, Horton starts fully recovered | |
| and Green-Ampt starts fully saturated. | |
| </div> | |
| <div class="icm-note"> | |
| <b>Groundwater:</b> ICM supports groundwater modeling but uses its own InfoWorks method rather | |
| than the SWMM5 two-zone groundwater equations. If your SWMM5 model has [GWF] sections, these | |
| need manual re-calibration in ICM. | |
| </div> | |
| <div class="icm-note"> | |
| <b>SWMM5 Import:</b> ICM can directly import SWMM5 .INP files. The importer automatically | |
| creates the Runoff Surfaces, Land Uses, and Subcatchment assignments. It creates one Runoff | |
| Surface per unique parameter set and shares them across subcatchments. | |
| </div> | |
| </div> | |
| <!-- Ruby Script for ICM SWMM Setup --> | |
| <div class="card" style="margin-bottom:14px;"> | |
| <h3>💎 Ruby Script: Auto-Configure SWMM Runoff Surfaces in ICM</h3> | |
| <div class="code"><span class="cm"># Ruby script to set up SWMM hydrology in ICM InfoWorks</span> | |
| <span class="cm"># Run from ICM UI Script → applies to current network</span> | |
| net = WSApplication.current_network | |
| <span class="cm"># --- Step 1: Define three SWMM Runoff Surfaces ---</span> | |
| net.transaction_begin | |
| <span class="cm"># Surface A1: Impervious, no depression storage</span> | |
| s1 = net.new_row_object(<span class="str">'hw_runoff_surface'</span>) | |
| s1[<span class="str">'surface_id'</span>] = <span class="str">'SWMM_Imperv_noDS'</span> | |
| s1[<span class="str">'routing_model'</span>] = <span class="str">'SWMM'</span> | |
| s1[<span class="str">'runoff_volume_type'</span>] = <span class="str">'Fixed'</span> | |
| s1[<span class="str">'fixed_runoff_coefficient'</span>] = <span class="num">1.0</span> | |
| s1[<span class="str">'runoff_routing_value'</span>] = <span class="num">0.012</span> <span class="cm"># Manning's n</span> | |
| s1[<span class="str">'initial_loss_type'</span>] = <span class="str">'Absolute'</span> | |
| s1[<span class="str">'initial_loss_value'</span>] = <span class="num">0.0</span> <span class="cm"># No depression storage</span> | |
| s1.write | |
| <span class="cm"># Surface A2: Impervious, with depression storage</span> | |
| s2 = net.new_row_object(<span class="str">'hw_runoff_surface'</span>) | |
| s2[<span class="str">'surface_id'</span>] = <span class="str">'SWMM_Imperv_wDS'</span> | |
| s2[<span class="str">'routing_model'</span>] = <span class="str">'SWMM'</span> | |
| s2[<span class="str">'runoff_volume_type'</span>] = <span class="str">'Fixed'</span> | |
| s2[<span class="str">'fixed_runoff_coefficient'</span>] = <span class="num">1.0</span> | |
| s2[<span class="str">'runoff_routing_value'</span>] = <span class="num">0.012</span> <span class="cm"># Manning's n</span> | |
| s2[<span class="str">'initial_loss_type'</span>] = <span class="str">'Absolute'</span> | |
| s2[<span class="str">'initial_loss_value'</span>] = <span class="num">0.05</span> <span class="cm"># 0.05 in depression storage</span> | |
| s2.write | |
| <span class="cm"># Surface A3: Pervious, Horton infiltration</span> | |
| s3 = net.new_row_object(<span class="str">'hw_runoff_surface'</span>) | |
| s3[<span class="str">'surface_id'</span>] = <span class="str">'SWMM_Pervious'</span> | |
| s3[<span class="str">'routing_model'</span>] = <span class="str">'SWMM'</span> | |
| s3[<span class="str">'runoff_volume_type'</span>] = <span class="str">'Horton'</span> | |
| s3[<span class="str">'runoff_routing_value'</span>] = <span class="num">0.15</span> <span class="cm"># Manning's n for overland</span> | |
| s3[<span class="str">'initial_loss_type'</span>] = <span class="str">'Absolute'</span> | |
| s3[<span class="str">'initial_loss_value'</span>] = <span class="num">0.20</span> <span class="cm"># 0.20 in depression storage</span> | |
| s3[<span class="str">'horton_max_rate'</span>] = <span class="num">3.0</span> <span class="cm"># f0 (in/hr)</span> | |
| s3[<span class="str">'horton_min_rate'</span>] = <span class="num">0.5</span> <span class="cm"># fc (in/hr)</span> | |
| s3[<span class="str">'horton_decay'</span>] = <span class="num">4.0</span> <span class="cm"># k (1/hr)</span> | |
| s3.write | |
| net.transaction_commit | |
| puts <span class="str">"Created 3 SWMM runoff surfaces"</span> | |
| <span class="cm"># --- Step 2: Create Land Use referencing all 3 surfaces ---</span> | |
| net.transaction_begin | |
| lu = net.new_row_object(<span class="str">'hw_land_use'</span>) | |
| lu[<span class="str">'land_use_id'</span>] = <span class="str">'SWMM_Mixed'</span> | |
| <span class="cm"># Set surface areas (example: 50% imperv, 25% of that has no DS)</span> | |
| <span class="cm"># A1 = 50% × 25% = 12.5%, A2 = 50% × 75% = 37.5%, A3 = 50%</span> | |
| lu.write | |
| net.transaction_commit | |
| <span class="cm"># --- Step 3: Assign to subcatchments ---</span> | |
| net.transaction_begin | |
| net.row_objects(<span class="str">'hw_subcatchment'</span>).each <span class="kw">do</span> |sc| | |
| sc[<span class="str">'land_use_id'</span>] = <span class="str">'SWMM_Mixed'</span> | |
| <span class="cm"># Width comes from SWMM5 subcatchment width</span> | |
| <span class="cm"># sc['width'] = 500 # ft — set per subcatchment</span> | |
| sc.write | |
| <span class="kw">end</span> | |
| net.transaction_commit | |
| puts <span class="str">"Assigned SWMM land use to all subcatchments"</span></div> | |
| </div> | |
| <!-- Architecture comparison diagram --> | |
| <div class="card"> | |
| <h3>🏛️ Architecture Comparison: Data Flow</h3> | |
| <div class="grid2"> | |
| <div> | |
| <div class="eq" style="font-size:0.68rem;"> | |
| <b class="swmm-blue">EPA SWMM5 Data Structure</b> | |
| [SUBCATCHMENTS] | |
| Name RainGage Outlet Area %Imperv Width Slope | |
| S1 RG1 J1 10 50 500 2.0 | |
| [SUBAREAS] | |
| Subcat N-Imperv N-Perv S-Imperv S-Perv %Zero Route | |
| S1 0.012 0.15 0.05 0.20 25 OUTLET | |
| [INFILTRATION] | |
| Subcat MaxRate MinRate Decay DryTime MaxInfil | |
| S1 3.0 0.5 4.0 7 0 | |
| <span class="cm">→ All 3 surfaces defined FLAT on the subcatchment</span> | |
| <span class="cm">→ One-level structure: Subcatchment has everything</span> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="eq" style="font-size:0.68rem;"> | |
| <b class="icm-orange">ICM InfoWorks Data Structure</b> | |
| <b style="color:#fb923c;">Runoff Surface</b> (hw_runoff_surface) | |
| SWMM_Imperv_noDS → routing=SWMM, n=0.012, ds=0 | |
| SWMM_Imperv_wDS → routing=SWMM, n=0.012, ds=0.05 | |
| SWMM_Pervious → routing=SWMM, n=0.15, Horton | |
| <b style="color:#fb923c;">Land Use</b> (hw_land_use) | |
| SWMM_Mixed → 12.5% Imperv_noDS | |
| → 37.5% Imperv_wDS | |
| → 50.0% Pervious | |
| <b style="color:#fb923c;">Subcatchment</b> (hw_subcatchment) | |
| S1 → land_use=SWMM_Mixed, area=10ac, width=500 | |
| <span class="cm">→ Three-level hierarchy: Subcatch → LandUse → Surface</span> | |
| <span class="cm">→ Surfaces are SHARED across subcatchments</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="icm-note" style="margin-top:14px;"> | |
| <b>Engman Table — Manning's n for Overland Flow (from ICM Help):</b> | |
| Concrete/asphalt: 0.011 (0.01–0.013) | | |
| Bare sand: 0.01 (0.01–0.016) | | |
| Gravel: 0.02 (0.012–0.033) | | |
| Range (natural): 0.13 (0.01–0.32) | | |
| Bluegrass sod: 0.45 (0.39–0.63) | | |
| Short grass: 0.15 (0.10–0.20) | | |
| Bermuda grass: 0.41 (0.30–0.48) | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| SWMM5 Complete Runoff Explorer • EPA SWMM 5.2 Public Domain • | |
| <a href="https://swmm5.org">swmm5.org</a> • | |
| A <a href="https://swmm5.org">Dickinson Canon</a> Vibe Coding App | |
| </footer> | |
| <script> | |
| // =================================================================== | |
| // TABS | |
| // =================================================================== | |
| document.querySelectorAll('.tbtn').forEach(b => b.addEventListener('click', () => { | |
| document.querySelectorAll('.tbtn').forEach(x => x.classList.remove('on')); | |
| document.querySelectorAll('.tpanel').forEach(x => x.classList.remove('on')); | |
| b.classList.add('on'); | |
| document.getElementById('t-' + b.dataset.t).classList.add('on'); | |
| if (b.dataset.t === 'flow') drawFlow(); | |
| if (b.dataset.t === 'rk') doRK(); | |
| if (b.dataset.t === 'sim') resetSim(); | |
| })); | |
| // =================================================================== | |
| // TAB 1: CODE ARCHITECTURE | |
| // =================================================================== | |
| const mods = [ | |
| {cat:'c',icon:'⚙️',t:'swmm5.c',f:'Core Engine',d:'Main entry point. swmm_step() calls runoff_execute() then routing_execute() each wet time step.', | |
| code:`<span class="cm">// swmm5.c — main loop</span>\n<span class="kw">void</span> <span class="fn">swmm_step</span>(<span class="tp">double</span>* elapsed) {\n <span class="fn">runoff_execute</span>();\n <span class="fn">routing_execute</span>(RouteModel, routeStep);\n}`}, | |
| {cat:'h',icon:'💧',t:'runoff.c',f:'Runoff Manager',d:'Iterates all subcatchments each wet time step. Calls subcatch_getRunoff() for each. Manages the runoff routing clock.', | |
| code:`<span class="cm">// runoff.c — runoff_execute()</span>\n<span class="kw">for</span> (j = <span class="num">0</span>; j < Nobjects[SUBCATCH]; j++) {\n <span class="fn">subcatch_getRunoff</span>(j, runoffStep);\n}\nOldRunoffTime = NewRunoffTime;`}, | |
| {cat:'h',icon:'🏞️',t:'subcatch.c',f:'3-Surface Hydrology',d:'Core water balance. Splits subcatchment into A1 (imperv no DS), A2 (imperv w/ DS), A3 (pervious). Calls odesolve() for each surface depth ODE independently.', | |
| code:`<span class="cm">// subcatch.c — THREE surfaces</span>\n<span class="cm">// A1: imperv, no depression storage</span>\n<span class="fn">odesolve</span>(tStep, &d1, <span class="fn">getDdDt_A1</span>);\n<span class="cm">// A2: imperv, with depression storage</span>\n<span class="fn">odesolve</span>(tStep, &d2, <span class="fn">getDdDt_A2</span>);\n<span class="cm">// A3: pervious</span>\n<span class="fn">odesolve</span>(tStep, &d3, <span class="fn">getDdDt_A3</span>);\n\ntotalQ = <span class="fn">manningQ</span>(d1) + <span class="fn">manningQ</span>(d2) + <span class="fn">manningQ</span>(d3);`}, | |
| {cat:'o',icon:'🧮',t:'odesolve.c',f:'Cash-Karp RK5 ODE',d:'Adaptive 5th-order Runge-Kutta. 6 stages per step. Embedded 4th-order error estimate. Called THREE times per subcatchment — once for each surface.', | |
| code:`<span class="cm">// odesolve.c — called 3× per subcatch</span>\n<span class="kw">void</span> <span class="fn">odesolve</span>(<span class="tp">double</span> tStep, <span class="tp">double</span>* y,\n <span class="kw">void</span> (*derivs)(<span class="tp">double</span>,<span class="tp">double</span>*,<span class="tp">double</span>*))\n{\n k1 = h * f(t, y);\n k2 = h * f(t+a2*h, y+b21*k1);\n <span class="cm">// ... through k6</span>\n y5 = y + c1*k1 + c3*k3 + c4*k4 + c6*k6;\n err = |y5 − y4| <span class="cm">→ adapt h</span>\n}`}, | |
| {cat:'i',icon:'🌱',t:'infil.c',f:'Infiltration (A3 only)',d:'Horton, Modified Horton, Green-Ampt, Modified Green-Ampt, or Curve Number. Only called for the pervious surface A3 during its getDdDt callback.', | |
| code:`<span class="cm">// infil.c — Horton (pervious only)</span>\nf(t) = fc + (f0 - fc) * exp(-kd * t);\n<span class="cm">// Limited by available rainfall</span>\nf_actual = min(f(t), rainfall);`}, | |
| {cat:'h',icon:'🌧️',t:'gage.c',f:'Rain Gage Processing',d:'Provides rainfall at each time step. Reads from time series, external files, or interface files.', | |
| code:`<span class="fn">gage_getPrecip</span>(gage, &interval)\n<span class="kw">return</span> Gage[gage].rainfall;`}, | |
| {cat:'h',icon:'☀️',t:'climate.c',f:'Evaporation',d:'Constant, monthly, time series, or temperature-dependent evaporation. Applied to A2 and A3 surfaces (not A1).', | |
| code:`<span class="fn">climate_getAdjEvap</span>(rate)\n<span class="kw">return</span> rate * monthAdj[Month-<span class="num">1</span>];`}, | |
| {cat:'h',icon:'🌊',t:'gwater.c',f:'Groundwater',d:'Two-zone groundwater. Upper zone receives infiltration from A3, lower zone contributes lateral flow to nodes.', | |
| code:`dTheta/dt = (infil - evap - perc) / Lu\ndWaterTable/dt = (perc - gwFlow) / phi`}, | |
| {cat:'h',icon:'❄️',t:'snow.c',f:'Snowmelt',d:'Degree-day snowmelt on three snow-pack areas (plowable, impervious, pervious). Melt becomes input to runoff surfaces.', | |
| code:`melt = Dhm * (Temp - Tbase)`}, | |
| {cat:'r',icon:'🔀',t:'routing.c',f:'Flow Routing',d:'Routes subcatchment outflows (q₁+q₂+q₃) through the conveyance network via Steady, Kinematic, or Dynamic Wave.', | |
| code:`<span class="kw">switch</span>(RouteModel) {\n <span class="kw">case</span> SF: <span class="fn">steadyflow_execute</span>(); <span class="kw">break</span>;\n <span class="kw">case</span> KW: <span class="fn">kinwave_execute</span>(); <span class="kw">break</span>;\n <span class="kw">case</span> DW: <span class="fn">dynwave_execute</span>(); <span class="kw">break</span>;\n}`}, | |
| {cat:'r',icon:'📐',t:'dynwave.c',f:'Dynamic Wave',d:'Full Saint-Venant equations. Continuity + Momentum via explicit finite differences. Handles surcharge, flooding, backwater.', | |
| code:`<span class="cm">// ∂A/∂t + ∂Q/∂x = q_lat</span>\n<span class="cm">// ∂Q/∂t + ∂(Q²/A)/∂x + gA·∂H/∂x + gA·Sf = 0</span>`}, | |
| {cat:'i',icon:'🧪',t:'surfqual.c',f:'Surface Quality',d:'Pollutant buildup and washoff. EMC, rating curve, exponential models. Connects to routing water quality.', | |
| code:`W = C1 * (runoff^C2) * B <span class="cm">// exponential</span>\nW = C1 * runoff <span class="cm">// EMC</span>`}, | |
| ]; | |
| function buildArch() { | |
| const g = document.getElementById('archGrid'); | |
| g.innerHTML = ''; | |
| const icoClass = {c:'ico-c',h:'ico-h',o:'ico-o',i:'ico-i',r:'ico-r'}; | |
| mods.forEach(m => { | |
| const c = document.createElement('div'); | |
| c.className = 'acard'; | |
| c.onclick = () => c.classList.toggle('exp'); | |
| c.innerHTML = `<div class="ahead"><div class="aico ${icoClass[m.cat]}">${m.icon}</div><div><div class="atitle">${m.t}</div><div class="afile">${m.f}</div></div></div><div class="adesc">${m.d}</div><div class="adetail"><div class="code">${m.code}</div></div>`; | |
| g.appendChild(c); | |
| }); | |
| } | |
| buildArch(); | |
| // =================================================================== | |
| // TAB 2: CALL FLOW SVG | |
| // =================================================================== | |
| function drawFlow() { | |
| const s = document.getElementById('flowSvg'); | |
| const B = (x,y,w,h,t1,t2,col) => `<g><rect x="${x}" y="${y}" width="${w}" height="${h}" rx="7" fill="#101a2c" stroke="${col}" stroke-width="1.4"/><text x="${x+w/2}" y="${y+h/2-6}" fill="${col}" font-size="11" font-family="IBM Plex Mono" font-weight="600" text-anchor="middle">${t1}</text><text x="${x+w/2}" y="${y+h/2+9}" fill="#94a3b8" font-size="9" font-family="IBM Plex Mono" text-anchor="middle">${t2}</text></g>`; | |
| const A = (x1,y1,x2,y2,c='#38bdf8') => `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${c}" stroke-width="1.4" marker-end="url(#a${c.replace('#','')})"/>`; | |
| const mkr = c => `<marker id="a${c.replace('#','')}" viewBox="0 0 10 6" refX="10" refY="3" markerWidth="7" markerHeight="5" orient="auto"><path d="M0,0 L10,3 L0,6 Z" fill="${c}"/></marker>`; | |
| s.innerHTML = `<defs>${mkr('#38bdf8')}${mkr('#34d399')}${mkr('#fbbf24')}${mkr('#818cf8')}</defs> | |
| ${B(80,20,160,48,'swmm5.c','swmm_step()','#f87171')} | |
| ${B(80,115,160,48,'runoff.c','runoff_execute()','#38bdf8')} | |
| ${B(80,230,180,55,'subcatch.c','subcatch_getRunoff()','#38bdf8')} | |
| ${B(400,195,170,48,'odesolve.c → A1','RK5 (no DS)','#34d399')} | |
| ${B(400,260,170,48,'odesolve.c → A2','RK5 (w/ DS)','#34d399')} | |
| ${B(400,325,170,48,'odesolve.c → A3','RK5 (pervious)','#34d399')} | |
| ${B(700,195,170,48,'getDdDt_A1','dd₁/dt = i − q₁','#38bdf8')} | |
| ${B(700,260,170,48,'getDdDt_A2','dd₂/dt = i−e − q₂','#38bdf8')} | |
| ${B(700,325,170,48,'getDdDt_A3','dd₃/dt = i−e−f − q₃','#38bdf8')} | |
| ${B(920,120,140,44,'gage.c','rainfall i(t)','#5dade2')} | |
| ${B(920,190,140,44,'climate.c','evap e(t)','#5dade2')} | |
| ${B(920,350,140,48,'infil.c','Horton f(t)','#fbbf24')} | |
| ${B(400,440,170,48,'Manning\'s Eq','q = W·(d−ds)^⁵⁄₃','#38bdf8')} | |
| ${B(80,530,160,48,'routing.c','routing_execute()','#818cf8')} | |
| ${B(400,530,170,48,'dynwave.c','St. Venant','#818cf8')} | |
| ${A(160,68,160,115)} ${A(160,163,160,230)} | |
| ${A(260,250,400,220,'#34d399')} ${A(260,260,400,284,'#34d399')} ${A(260,275,400,349,'#34d399')} | |
| ${A(570,220,700,220)} ${A(570,284,700,284)} ${A(570,349,700,349)} | |
| ${A(870,220,920,145)} ${A(870,284,920,215)} ${A(870,349,920,374,'#fbbf24')} | |
| <path d="M 570 210 Q 590 160 530 155 Q 470 150 400 210" fill="none" stroke="#34d399" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#a34d399)"/> | |
| <text x="490" y="147" fill="#34d399" font-size="9" font-family="IBM Plex Mono" text-anchor="middle">6 stages each</text> | |
| ${A(400,373,400,440,'#38bdf8')} | |
| ${A(160,285,160,530)} | |
| ${A(240,555,400,555,'#818cf8')} | |
| <text x="295" y="240" fill="#34d399" font-size="8.5" font-family="IBM Plex Mono">solve dd₁/dt</text> | |
| <text x="295" y="278" fill="#34d399" font-size="8.5" font-family="IBM Plex Mono">solve dd₂/dt</text> | |
| <text x="295" y="343" fill="#34d399" font-size="8.5" font-family="IBM Plex Mono">solve dd₃/dt</text> | |
| <text x="630" y="212" fill="#38bdf8" font-size="8.5" font-family="IBM Plex Mono">callback</text> | |
| <text x="630" y="278" fill="#38bdf8" font-size="8.5" font-family="IBM Plex Mono">callback</text> | |
| <text x="630" y="343" fill="#38bdf8" font-size="8.5" font-family="IBM Plex Mono">callback</text> | |
| <text x="168" y="205" fill="#38bdf8" font-size="8.5" font-family="IBM Plex Mono">for each subcatch</text> | |
| <text x="168" y="420" fill="#818cf8" font-size="8.5" font-family="IBM Plex Mono">q₁+q₂+q₃ → nodes</text> | |
| `; | |
| } | |
| // =================================================================== | |
| // CASH-KARP RK5 SOLVER | |
| // =================================================================== | |
| const CK = { | |
| a:[0,1/5,3/10,3/5,1,7/8], | |
| b:[[],[1/5],[3/40,9/40],[3/10,-9/10,6/5],[-11/54,5/2,-70/27,35/27],[1631/55296,175/512,575/13824,44275/110592,253/4096]], | |
| c5:[37/378,0,250/621,125/594,0,512/1771], | |
| c4:[2825/27648,0,18575/48384,13525/55296,277/14336,1/4] | |
| }; | |
| function manQ(d,ds,n,W,S) { | |
| const e = d - ds; return e > 0 ? W*(1.49/n)*Math.pow(e,5/3)*Math.sqrt(S) : 0; | |
| } | |
| function rk5Step(t,d,h,deriv) { | |
| const k = new Array(6); | |
| k[0] = h * deriv(t, d); | |
| for (let i=1;i<6;i++) { let yi=d; for(let j=0;j<i;j++) yi+=CK.b[i][j]*k[j]; k[i]=h*deriv(t+CK.a[i]*h,Math.max(0,yi)); } | |
| let y5=d,y4=d; | |
| for(let i=0;i<6;i++){y5+=CK.c5[i]*k[i];y4+=CK.c4[i]*k[i];} | |
| y5=Math.max(0,y5); y4=Math.max(0,y4); | |
| return {y5,y4,err:Math.abs(y5-y4)/(Math.abs(y5)+1e-30),k}; | |
| } | |
| function rk5Solve(tStep,d0,deriv,captureK) { | |
| const TOL=1e-5; let t=0,d=d0,h=tStep,iter=0; | |
| while(t<tStep-1e-12&&iter<300){ | |
| if(t+h>tStep) h=tStep-t; | |
| const r=rk5Step(t,d,h,deriv); | |
| if(r.err>TOL&&h>0.05){h=Math.max(0.1*h,0.9*h*Math.pow(TOL/r.err,0.25));continue;} | |
| d=r.y5; t+=h; iter++; | |
| if(captureK&&iter===1) captureK(r.k); | |
| if(r.err>1e-30) h=Math.min(5*h,Math.min(tStep-t+0.01,0.9*h*Math.pow(TOL/r.err,0.2))); | |
| } | |
| return {d,steps:iter}; | |
| } | |
| // =================================================================== | |
| // TAB 3: RK5 INTERACTIVE | |
| // =================================================================== | |
| function doRK(){ | |
| const surf=document.getElementById('rkSurfSel').value; | |
| const d0=+document.getElementById('rkD0').value; | |
| const rain=+document.getElementById('rkR').value; | |
| const dt=+document.getElementById('rkDT').value; | |
| const n=+document.getElementById('rkN').value; | |
| const ds=+document.getElementById('rkDS').value; | |
| document.getElementById('rkD0v').textContent=d0.toFixed(3); | |
| document.getElementById('rkRv').textContent=rain.toFixed(1); | |
| document.getElementById('rkDTv').textContent=dt; | |
| document.getElementById('rkNv').textContent=n.toFixed(3); | |
| document.getElementById('rkDSv').textContent=ds.toFixed(2); | |
| document.getElementById('rkSurf').textContent={a1:'A1 Imperv (no DS)',a2:'A2 Imperv (w/ DS)',a3:'A3 Pervious'}[surf]; | |
| const W=500, S=0.02, area=10*43560, f0=3, fc=0.5, kd=4; | |
| const dsFt=surf==='a1'?0:ds/12; | |
| const deriv=(t,d)=>{ | |
| let netRain=rain; | |
| if(surf==='a3') netRain=Math.max(0,rain-Math.min(rain,fc+(f0-fc)*Math.exp(-kd*t/3600))); | |
| return netRain/12/3600 - manQ(d,dsFt,n,W,S)/area; | |
| }; | |
| // Single step display | |
| const disp=rk5Step(0,d0,dt,deriv); | |
| for(let i=0;i<6;i++){ | |
| document.getElementById('rv'+(i+1)).textContent=(disp.k[i]*1e4).toFixed(3)+'e-4'; | |
| const box=document.getElementById('rk-k'+(i+1)); | |
| box.classList.remove('lit'); | |
| setTimeout(()=>box.classList.add('lit'),i*120); | |
| } | |
| // Full adaptive solve | |
| let steps=0; | |
| const res=rk5Solve(dt,d0,deriv,null); | |
| const qOut=manQ(res.d,dsFt,n,W,S); | |
| document.getElementById('rkStats').innerHTML=` | |
| <div class="st"><div class="sv" style="color:var(--green)">${disp.y5.toFixed(6)}</div><div class="sl">d (5th order) ft</div></div> | |
| <div class="st"><div class="sv" style="color:var(--gold)">${disp.y4.toFixed(6)}</div><div class="sl">d (4th order) ft</div></div> | |
| <div class="st"><div class="sv" style="color:var(--red)">${disp.err.toExponential(2)}</div><div class="sl">Truncation Error</div></div> | |
| <div class="st"><div class="sv" style="color:var(--accent)">${res.d.toFixed(6)}</div><div class="sl">d (adaptive) ft</div></div> | |
| <div class="st"><div class="sv" style="color:var(--accent)">${qOut.toFixed(3)}</div><div class="sl">Outflow (cfs)</div></div> | |
| <div class="st"><div class="sv" style="color:var(--accent2)">${res.steps}</div><div class="sl">Sub-steps</div></div>`; | |
| } | |
| // =================================================================== | |
| // TAB 4: THREE-SURFACE SIMULATOR | |
| // =================================================================== | |
| const SCS={ | |
| scs2:[0,.011,.022,.035,.048,.063,.08,.098,.12,.147,.181,.235,.663,.772,.82,.854,.88,.898,.918,.936,.953,.969,.981,.991,1], | |
| scs1:[0,.017,.035,.054,.076,.099,.126,.156,.194,.254,.515,.583,.624,.654,.682,.706,.727,.748,.767,.785,.801,.825,.86,.916,1], | |
| scs1a:[0,.02,.05,.082,.116,.156,.206,.268,.425,.48,.52,.55,.577,.601,.624,.645,.664,.683,.701,.719,.736,.753,.771,.792,1], | |
| scs3:[0,.01,.02,.032,.045,.06,.077,.095,.116,.14,.17,.206,.515,.583,.643,.7,.751,.79,.822,.85,.876,.899,.928,.962,1] | |
| }; | |
| function genStorm(type,P,D){ | |
| const N=Math.max(48,Math.round(D*4)),dt=D/N,rain=new Array(N); | |
| if(SCS[type]){const tbl=SCS[type];for(let i=0;i<N;i++){const tf=(i+.5)/N,idx=tf*24,lo=Math.floor(idx),hi=Math.min(lo+1,24);rain[i]=Math.max(0,(tbl[hi]-tbl[lo])*P*(24/N)/dt);}} | |
| else if(type==='chicago'){const r=.4,a=P*.8,b=.8;for(let i=0;i<N;i++){const t=(i+.5)*dt,tp=r*D,tb=Math.abs(t-tp);rain[i]=a/Math.pow(tb/b+1,2)/D*3;}const s=rain.reduce((a,b)=>a+b,0)*dt;for(let i=0;i<N;i++)rain[i]*=P/s;} | |
| else if(type==='uniform'){rain.fill(P/D);} | |
| else{const tp=D*.4,pk=2*P/D;for(let i=0;i<N;i++){const t=(i+.5)*dt;rain[i]=Math.max(0,t<=tp?pk*t/tp:pk*(1-(t-tp)/(D-tp)));}} | |
| return{rain,N,dt}; | |
| } | |
| let sim=null, animId=null, animStep=0, running=false, rainDrops=[]; | |
| function P(){ | |
| const g=id=>+document.getElementById(id).value; | |
| return{type:document.getElementById('sType').value,P:g('sP'),D:g('sD'),area:g('sA'),slope:g('sSl')/100, | |
| pctImp:g('sImp')/100,pctNds:g('sNds')/100,n1:g('sN1'),w1:g('sW1'),n2:g('sN2'),ds2:g('sDS2'),w2:g('sW2'), | |
| n3:g('sN3'),ds3:g('sDS3'),w3:g('sW3'),f0:g('sF0'),fc:g('sFc'),kd:g('sKd')}; | |
| } | |
| function upLabels(){ | |
| const p=P(); | |
| document.getElementById('vP').textContent=p.P.toFixed(1)+' in'; | |
| document.getElementById('vD').textContent=p.D+' hr'; | |
| document.getElementById('vA').textContent=p.area+' ac'; | |
| document.getElementById('vSl').textContent=(p.slope*100).toFixed(1)+'%'; | |
| document.getElementById('vImp').textContent=Math.round(p.pctImp*100)+'%'; | |
| document.getElementById('vNds').textContent=Math.round(p.pctNds*100)+'%'; | |
| document.getElementById('vN1').textContent=p.n1.toFixed(3); | |
| document.getElementById('vW1').textContent=p.w1+' ft'; | |
| document.getElementById('vN2').textContent=p.n2.toFixed(3); | |
| document.getElementById('vDS2').textContent=p.ds2.toFixed(3)+' in'; | |
| document.getElementById('vW2').textContent=p.w2+' ft'; | |
| document.getElementById('vN3').textContent=p.n3.toFixed(3); | |
| document.getElementById('vDS3').textContent=p.ds3.toFixed(3)+' in'; | |
| document.getElementById('vW3').textContent=p.w3+' ft'; | |
| document.getElementById('vF0').textContent=p.f0.toFixed(1)+' in/hr'; | |
| document.getElementById('vFc').textContent=p.fc.toFixed(2)+' in/hr'; | |
| document.getElementById('vKd').textContent=p.kd.toFixed(1)+' /hr'; | |
| } | |
| function runSim(){ | |
| const p=P(); | |
| const storm=genStorm(p.type,p.P,p.D); | |
| const{rain,N,dt}=storm,dtS=dt*3600; | |
| const fA1=p.pctImp*p.pctNds, fA2=p.pctImp*(1-p.pctNds), fA3=1-p.pctImp; | |
| const aFt2=p.area*43560; | |
| let d1=0,d2=0,d3=0; | |
| const h={t:[],rain:[],d1:[],d2:[],d3:[],q1:[],q2:[],q3:[],qt:[],infil:[],steps1:[],steps2:[],steps3:[], | |
| kA1:[],kA2:[],kA3:[]}; | |
| for(let i=0;i<N;i++){ | |
| const iR=rain[i], tHr=i*dt; | |
| const fI=Math.min(iR, p.fc+(p.f0-p.fc)*Math.exp(-p.kd*tHr)); | |
| let ks1=null,ks2=null,ks3=null; | |
| const A1=fA1*aFt2; | |
| const r1= A1>0 ? rk5Solve(dtS,d1,(t,d)=>iR/12/3600 - manQ(d,0,p.n1,p.w1,p.slope)/A1, k=>{ks1=k.slice();}) : {d:0,steps:0}; | |
| d1=r1.d; | |
| const A2=fA2*aFt2, ds2f=p.ds2/12; | |
| const r2= A2>0 ? rk5Solve(dtS,d2,(t,d)=>iR/12/3600 - manQ(d,ds2f,p.n2,p.w2,p.slope)/A2, k=>{ks2=k.slice();}) : {d:0,steps:0}; | |
| d2=r2.d; | |
| const A3=fA3*aFt2, ds3f=p.ds3/12; | |
| const r3= A3>0 ? rk5Solve(dtS,d3,(t,d)=>Math.max(0,iR-fI)/12/3600 - manQ(d,ds3f,p.n3,p.w3,p.slope)/A3, k=>{ks3=k.slice();}) : {d:0,steps:0}; | |
| d3=r3.d; | |
| const q1=A1>0?manQ(d1,0,p.n1,p.w1,p.slope):0; | |
| const q2=A2>0?manQ(d2,ds2f,p.n2,p.w2,p.slope):0; | |
| const q3=A3>0?manQ(d3,ds3f,p.n3,p.w3,p.slope):0; | |
| h.t.push(tHr+dt/2); h.rain.push(iR); h.d1.push(d1*12); h.d2.push(d2*12); h.d3.push(d3*12); | |
| h.q1.push(q1); h.q2.push(q2); h.q3.push(q3); h.qt.push(q1+q2+q3); h.infil.push(fI); | |
| h.steps1.push(r1.steps); h.steps2.push(r2.steps); h.steps3.push(r3.steps); | |
| h.kA1.push(ks1); h.kA2.push(ks2); h.kA3.push(ks3); | |
| } | |
| sim={...h,N,dt,p,fA1,fA2,fA3}; | |
| } | |
| function resetSim(){ | |
| upLabels(); | |
| if(running){cancelAnimationFrame(animId);running=false;} | |
| animStep=0; rainDrops=[]; | |
| runSim(); drawViz(0); drawCharts(0); updateSS(0); | |
| document.getElementById('btnPlay').textContent='▶ Run Animation'; | |
| document.getElementById('btnPlay').className='btn btn-go'; | |
| } | |
| function toggleAnim(){ | |
| if(running){cancelAnimationFrame(animId);running=false; | |
| document.getElementById('btnPlay').textContent='▶ Run Animation'; | |
| document.getElementById('btnPlay').className='btn btn-go'; | |
| } else { | |
| if(!sim) runSim(); | |
| running=true; | |
| document.getElementById('btnPlay').textContent='⏸ Pause'; | |
| document.getElementById('btnPlay').className='btn btn-stop'; | |
| doAnim(); | |
| } | |
| } | |
| function doAnim(){ | |
| if(!running||!sim) return; | |
| const spd=Math.max(1,Math.floor(sim.N/350)); | |
| animStep=Math.min(animStep+spd,sim.N-1); | |
| drawViz(animStep); updateSS(animStep); updateRKLive(animStep); | |
| if(animStep%3===0) drawCharts(animStep); | |
| if(animStep>=sim.N-1){running=false;drawCharts(sim.N-1); | |
| document.getElementById('btnPlay').textContent='▶ Replay'; | |
| document.getElementById('btnPlay').className='btn btn-go';animStep=0;return;} | |
| animId=requestAnimationFrame(doAnim); | |
| } | |
| function updateSS(i){ | |
| if(!sim) return; | |
| const s=sim; | |
| document.getElementById('vizTime').textContent=`t = ${s.t[i].toFixed(1)} hr`; | |
| const pk=Math.max(...s.qt)||1; | |
| document.getElementById('simStats').innerHTML=` | |
| <div class="st"><div class="sv" style="color:#546E7A">${s.q1[i].toFixed(2)}</div><div class="sl">A1 Q (cfs)</div></div> | |
| <div class="st"><div class="sv" style="color:#78909C">${s.q2[i].toFixed(2)}</div><div class="sl">A2 Q (cfs)</div></div> | |
| <div class="st"><div class="sv" style="color:#4E7A3E">${s.q3[i].toFixed(2)}</div><div class="sl">A3 Q (cfs)</div></div> | |
| <div class="st"><div class="sv" style="color:var(--accent)">${s.qt[i].toFixed(2)}</div><div class="sl">Total Q (cfs)</div></div> | |
| <div class="st"><div class="sv" style="color:#5dade2">${s.rain[i].toFixed(2)}</div><div class="sl">Rain (in/hr)</div></div> | |
| <div class="st"><div class="sv" style="color:#FFB74D">${s.infil[i].toFixed(2)}</div><div class="sl">Infil (in/hr)</div></div> | |
| <div class="st"><div class="sv" style="color:var(--accent)">${s.d1[i].toFixed(4)}</div><div class="sl">d₁ (in)</div></div> | |
| <div class="st"><div class="sv" style="color:var(--accent)">${s.d2[i].toFixed(4)}</div><div class="sl">d₂ (in)</div></div> | |
| <div class="st"><div class="sv" style="color:#66BB6A">${s.d3[i].toFixed(4)}</div><div class="sl">d₃ (in)</div></div>`; | |
| } | |
| function updateRKLive(i){ | |
| if(!sim) return; | |
| const s=sim; | |
| [['rkLive1',s.kA1[i],'#546E7A'],['rkLive2',s.kA2[i],'#78909C'],['rkLive3',s.kA3[i],'#4E7A3E']].forEach(([id,ks,col])=>{ | |
| const el=document.getElementById(id); | |
| if(!ks){el.innerHTML='<div style="font-size:0.65rem;color:var(--txt3);grid-column:1/-1;">N/A (0% area)</div>';return;} | |
| el.innerHTML=ks.map((v,j)=>`<div class="rkbox lit" style="border-color:${col};padding:4px;"><div style="font-size:0.6rem;color:${col};font-weight:600;">k${j+1}</div><div style="font-size:0.58rem;color:var(--txt2);margin-top:1px;">${(v*1e5).toFixed(1)}e-5</div></div>`).join(''); | |
| }); | |
| document.getElementById('rkLiveStats').innerHTML=` | |
| <div class="st"><div class="sv" style="color:#546E7A;font-size:0.85rem;">${s.steps1[i]}</div><div class="sl">A1 sub-steps</div></div> | |
| <div class="st"><div class="sv" style="color:#78909C;font-size:0.85rem;">${s.steps2[i]}</div><div class="sl">A2 sub-steps</div></div> | |
| <div class="st"><div class="sv" style="color:#4E7A3E;font-size:0.85rem;">${s.steps3[i]}</div><div class="sl">A3 sub-steps</div></div>`; | |
| } | |
| // =================================================================== | |
| // ANIMATED CROSS-SECTION | |
| // =================================================================== | |
| function drawViz(step){ | |
| if(!sim) return; | |
| const s=sim, canvas=document.getElementById('cviz'); | |
| const W=canvas.parentElement.clientWidth, H=340; | |
| canvas.width=W*2; canvas.height=H*2; canvas.style.width=W+'px'; canvas.style.height=H+'px'; | |
| const ctx=canvas.getContext('2d'); ctx.scale(2,2); | |
| const maxR=Math.max(...s.rain)||1, rr=s.rain[step]||0, dark=Math.min(1,rr/maxR); | |
| // Sky | |
| const sg=ctx.createLinearGradient(0,0,0,H*.55); | |
| sg.addColorStop(0,lerp('#162544','#060a14',dark*.6)); | |
| sg.addColorStop(1,lerp('#1e3a5f','#101825',dark*.5)); | |
| ctx.fillStyle=sg; ctx.fillRect(0,0,W,H*.55); | |
| // Clouds | |
| if(rr>0){ctx.fillStyle=`rgba(100,120,150,${.12+dark*.2})`; | |
| for(let c=0;c<5;c++){const cx=W*(.1+c*.2),cy=22+c*7,r=22+dark*14; | |
| ctx.beginPath();ctx.ellipse(cx,cy,r,r*.35,0,0,Math.PI*2);ctx.ellipse(cx-r*.5,cy+4,r*.6,r*.28,0,0,Math.PI*2);ctx.fill();}} | |
| // Rain | |
| if(rr>0){const nd=Math.floor(rr/maxR*25+2); | |
| for(let i=0;i<nd;i++) rainDrops.push({x:Math.random()*W,y:Math.random()*30,vy:4+Math.random()*6,l:5+Math.random()*9});} | |
| ctx.strokeStyle=`rgba(93,173,226,${.25+dark*.35})`; ctx.lineWidth=1; | |
| rainDrops.forEach(d=>{ctx.beginPath();ctx.moveTo(d.x,d.y);ctx.lineTo(d.x-.4,d.y+d.l);ctx.stroke();d.y+=d.vy;}); | |
| rainDrops=rainDrops.filter(d=>d.y<H*.55); | |
| // Ground | |
| const gY=H*.55, gH=H-gY; | |
| const w1=s.fA1*W, w2=s.fA2*W, w3=s.fA3*W; | |
| let x=0; | |
| // A1 | |
| if(w1>0){ | |
| ctx.fillStyle='#37474F'; ctx.fillRect(x,gY,w1,gH); | |
| ctx.strokeStyle='rgba(255,255,255,.04)'; ctx.lineWidth=.5; | |
| for(let ly=gY+7;ly<H;ly+=10){ctx.beginPath();ctx.moveTo(x,ly);ctx.lineTo(x+w1,ly);ctx.stroke();} | |
| const dp=Math.min(gH*.5,s.d1[step]*180); | |
| if(dp>.2){const wg=ctx.createLinearGradient(0,gY-dp,0,gY);wg.addColorStop(0,'rgba(33,150,243,.45)');wg.addColorStop(1,'rgba(21,101,192,.65)');ctx.fillStyle=wg;ctx.fillRect(x,gY-dp,w1,dp);} | |
| ctx.fillStyle='#B0BEC5'; ctx.font='600 9px IBM Plex Mono'; ctx.textAlign='center'; | |
| ctx.fillText('A1 Imperv (no DS)',x+w1/2,gY+16); | |
| if(s.q1[step]>.01) drawArr(ctx,x+w1-4,gY-dp/2,Math.min(25,s.q1[step]/Math.max(...s.qt)*20+3),'#546E7A'); | |
| x+=w1; | |
| } | |
| // A2 | |
| if(w2>0){ | |
| ctx.fillStyle='#455A64'; ctx.fillRect(x,gY,w2,gH); | |
| const dsL=Math.min(gH*.12,s.p.ds2*14); | |
| ctx.strokeStyle='rgba(255,183,77,.45)'; ctx.lineWidth=1; ctx.setLineDash([3,3]); | |
| ctx.beginPath();ctx.moveTo(x,gY-dsL);ctx.lineTo(x+w2,gY-dsL);ctx.stroke(); ctx.setLineDash([]); | |
| ctx.fillStyle='rgba(255,183,77,.55)'; ctx.font='8px IBM Plex Mono'; ctx.textAlign='left'; | |
| ctx.fillText('ds₂',x+3,gY-dsL-2); | |
| const dp=Math.min(gH*.5,s.d2[step]*180); | |
| if(dp>.2){const wg=ctx.createLinearGradient(0,gY-dp,0,gY);wg.addColorStop(0,'rgba(33,150,243,.4)');wg.addColorStop(1,'rgba(21,101,192,.6)');ctx.fillStyle=wg;ctx.fillRect(x,gY-dp,w2,dp);} | |
| ctx.fillStyle='#90A4AE'; ctx.font='600 9px IBM Plex Mono'; ctx.textAlign='center'; | |
| ctx.fillText('A2 Imperv (w/ DS)',x+w2/2,gY+16); | |
| if(s.q2[step]>.01) drawArr(ctx,x+w2-4,gY-dp/2,Math.min(25,s.q2[step]/Math.max(...s.qt)*20+3),'#78909C'); | |
| x+=w2; | |
| } | |
| // A3 | |
| if(w3>0){ | |
| const sg2=ctx.createLinearGradient(0,gY,0,H); | |
| sg2.addColorStop(0,'#4E7A3E');sg2.addColorStop(.15,'#3E6A2E');sg2.addColorStop(.4,'#8D6E63');sg2.addColorStop(1,'#5D4037'); | |
| ctx.fillStyle=sg2; ctx.fillRect(x,gY,w3,gH); | |
| // Grass | |
| ctx.strokeStyle='#66BB6A'; ctx.lineWidth=1.3; | |
| for(let g=x+7;g<x+w3-4;g+=12){const gh=3+Math.random()*4;ctx.beginPath();ctx.moveTo(g,gY);ctx.lineTo(g-1.5,gY-gh);ctx.stroke();} | |
| const dsL=Math.min(gH*.15,s.p.ds3*10); | |
| ctx.strokeStyle='rgba(255,183,77,.45)'; ctx.lineWidth=1; ctx.setLineDash([3,3]); | |
| ctx.beginPath();ctx.moveTo(x,gY-dsL);ctx.lineTo(x+w3,gY-dsL);ctx.stroke(); ctx.setLineDash([]); | |
| ctx.fillStyle='rgba(255,183,77,.55)'; ctx.font='8px IBM Plex Mono'; ctx.textAlign='left'; | |
| ctx.fillText('ds₃',x+3,gY-dsL-2); | |
| const dp=Math.min(gH*.5,s.d3[step]*130); | |
| if(dp>.2){const wg=ctx.createLinearGradient(0,gY-dp,0,gY);wg.addColorStop(0,'rgba(33,150,243,.3)');wg.addColorStop(1,'rgba(21,101,192,.5)');ctx.fillStyle=wg;ctx.fillRect(x,gY-dp,w3,dp);} | |
| // Infil arrows | |
| if(s.infil[step]>.01){ | |
| const nA=Math.floor(s.infil[step]/Math.max(...s.infil)*5+1); | |
| ctx.strokeStyle='rgba(255,183,77,.65)'; ctx.lineWidth=1.3; | |
| for(let a=0;a<nA;a++){const ax=x+w3*(.15+a*.7/nA),aL=10+s.infil[step]/Math.max(...s.infil)*16; | |
| ctx.beginPath();ctx.moveTo(ax,gY+3);ctx.lineTo(ax,gY+3+aL);ctx.stroke(); | |
| ctx.beginPath();ctx.moveTo(ax-2.5,gY+aL);ctx.lineTo(ax,gY+3+aL);ctx.lineTo(ax+2.5,gY+aL);ctx.stroke();} | |
| ctx.fillStyle='rgba(255,183,77,.75)'; ctx.font='8px IBM Plex Mono'; ctx.textAlign='center'; | |
| ctx.fillText('f='+s.infil[step].toFixed(1)+' in/hr',x+w3/2,gY+44); | |
| } | |
| ctx.fillStyle='#81C784'; ctx.font='600 9px IBM Plex Mono'; ctx.textAlign='center'; | |
| ctx.fillText('A3 Pervious',x+w3/2,gY+16); | |
| if(s.q3[step]>.01) drawArr(ctx,x+w3-4,gY-dp/2,Math.min(25,s.q3[step]/Math.max(...s.qt)*20+3),'#4E7A3E'); | |
| } | |
| // Dividers | |
| ctx.strokeStyle='rgba(255,255,255,.12)'; ctx.lineWidth=1; ctx.setLineDash([]); | |
| if(w1>0&&w2>0){ctx.beginPath();ctx.moveTo(w1,gY-15);ctx.lineTo(w1,H);ctx.stroke();} | |
| if(w2>0&&w3>0){ctx.beginPath();ctx.moveTo(w1+w2,gY-15);ctx.lineTo(w1+w2,H);ctx.stroke();} | |
| // Outlet | |
| ctx.fillStyle='#FF7043'; ctx.beginPath(); ctx.arc(W-12,gY,7,0,Math.PI*2); ctx.fill(); | |
| ctx.fillStyle='#BF360C'; ctx.beginPath(); ctx.arc(W-12,gY,4,0,Math.PI*2); ctx.fill(); | |
| // Rain bar | |
| const barH=(rr/maxR)*35; | |
| ctx.fillStyle='rgba(93,173,226,.25)'; ctx.fillRect(8,8,10,35); | |
| ctx.fillStyle='rgba(93,173,226,.75)'; ctx.fillRect(8,43-barH,10,barH); | |
| ctx.fillStyle='#5dade2'; ctx.font='9px IBM Plex Mono'; ctx.textAlign='left'; | |
| ctx.fillText(rr.toFixed(1)+' in/hr',22,30); | |
| } | |
| function drawArr(ctx,x,y,sz,col){ctx.save();ctx.fillStyle=col;ctx.globalAlpha=.75;ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(x+sz,y-sz*.3);ctx.lineTo(x+sz*.7,y);ctx.lineTo(x+sz,y+sz*.3);ctx.closePath();ctx.fill();ctx.globalAlpha=1;ctx.restore();} | |
| function lerp(a,b,t){const pa=[parseInt(a.slice(1,3),16),parseInt(a.slice(3,5),16),parseInt(a.slice(5,7),16)];const pb=[parseInt(b.slice(1,3),16),parseInt(b.slice(3,5),16),parseInt(b.slice(5,7),16)];return`rgb(${Math.round(pa[0]+(pb[0]-pa[0])*t)},${Math.round(pa[1]+(pb[1]-pa[1])*t)},${Math.round(pa[2]+(pb[2]-pa[2])*t)})`;} | |
| // =================================================================== | |
| // CHARTS | |
| // =================================================================== | |
| function drawCharts(end){ | |
| if(!sim) return; | |
| const s=sim, e=Math.min(end+1,s.N); | |
| lineChart('c1',s.t,[{d:s.rain,c:'rgba(93,173,226,.35)',bar:1},{d:s.qt,c:'#4FC3F7',l:'Total Q'}],e,210); | |
| lineChart('c2',s.t,[{d:s.q1,c:'#546E7A',l:'A1'},{d:s.q2,c:'#78909C',l:'A2'},{d:s.q3,c:'#66BB6A',l:'A3'}],e,210); | |
| lineChart('c3',s.t,[{d:s.d1,c:'#546E7A',l:'d₁'},{d:s.d2,c:'#78909C',l:'d₂'},{d:s.d3,c:'#66BB6A',l:'d₃'}],e,195); | |
| lineChart('c4',s.t,[{d:s.infil,c:'#FFB74D',l:'Infil'},{d:s.steps1,c:'#546E7A',l:'A1 steps',dash:1},{d:s.steps3,c:'#66BB6A',l:'A3 steps',dash:1}],e,195); | |
| } | |
| function lineChart(id,xd,series,end,ht){ | |
| const cv=document.getElementById(id),W=cv.parentElement.clientWidth-32; | |
| cv.width=W*2;cv.height=ht*2;cv.style.width=W+'px';cv.style.height=ht+'px'; | |
| const ctx=cv.getContext('2d');ctx.scale(2,2);ctx.clearRect(0,0,W,ht); | |
| const p={l:46,r:10,t:20,b:28},cw=W-p.l-p.r,ch=ht-p.t-p.b; | |
| let mx=.001; series.forEach(s=>{for(let i=0;i<end;i++)mx=Math.max(mx,s.d[i]||0);}); mx*=1.15; | |
| ctx.strokeStyle='#162036';ctx.lineWidth=.5; | |
| for(let i=0;i<=4;i++){const y=p.t+ch*i/4;ctx.beginPath();ctx.moveTo(p.l,y);ctx.lineTo(p.l+cw,y);ctx.stroke();} | |
| series.forEach(s=>{ | |
| if(s.bar){const bw=Math.max(1,cw/xd.length*.7);ctx.fillStyle=s.c;for(let i=0;i<end;i++){const x=p.l+(i+.5)/xd.length*cw-bw/2,h=(s.d[i]/mx)*ch;ctx.fillRect(x,p.t,bw,h);}} | |
| else{ctx.strokeStyle=s.c;ctx.lineWidth=1.4;if(s.dash)ctx.setLineDash([4,3]);else ctx.setLineDash([]);ctx.beginPath();for(let i=0;i<end;i++){const x=p.l+(i+.5)/xd.length*cw,y=p.t+ch-(s.d[i]/mx)*ch;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}ctx.stroke();ctx.setLineDash([]);} | |
| }); | |
| ctx.font='8.5px IBM Plex Mono';let lx=p.l+3; | |
| series.forEach(s=>{if(!s.l)return;ctx.fillStyle=s.c.replace(/[\d.]+\)$/,'1)');ctx.fillRect(lx,p.t-11,12,2.5);ctx.fillStyle='#8fa4be';ctx.textAlign='left';ctx.fillText(s.l,lx+15,p.t-7);lx+=ctx.measureText(s.l).width+30;}); | |
| ctx.fillStyle='#5a7090';ctx.font='7.5px IBM Plex Mono';ctx.textAlign='center'; | |
| const nt=Math.min(7,xd.length);for(let i=0;i<nt;i++){const idx=Math.floor(i/nt*(xd.length-1)),x=p.l+(idx+.5)/xd.length*cw;ctx.fillText(xd[idx].toFixed(0)+'h',x,p.t+ch+12);} | |
| ctx.textAlign='right';for(let i=0;i<=4;i++)ctx.fillText((mx*(4-i)/4).toFixed(1),p.l-3,p.t+ch*i/4+3); | |
| } | |
| // Init live RK boxes | |
| ['rkLive1','rkLive2','rkLive3'].forEach(id=>{ | |
| document.getElementById(id).innerHTML=Array.from({length:6},(_,i)=>`<div class="rkbox" style="padding:4px;"><div style="font-size:0.6rem;color:var(--txt3);font-weight:600;">k${i+1}</div><div style="font-size:0.58rem;color:var(--txt3);">—</div></div>`).join(''); | |
| }); | |
| // Boot | |
| upLabels(); runSim(); drawViz(0); drawCharts(0); updateSS(0); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment