<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <title>Kerbal Orbits</title> <script type="text/javascript"> // http://www.nihilogic.dk/labs/canvas2image/ // https://developer.mozilla.org/en/Drawing_Graphics_with_Canvas // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#the-2d-context // http://www.w3schools.com/jsref/default.asp var mainCanvasCtx, mainCanvas; var mouseIsDown = false; var mouseIsDragging = false; var cursorClickX = 0, cursorClickY = 0; var cursorLastX = 0, cursorLastY = 0; var pi = Math.PI; var phi = (1 + Math.sqrt(5))/2; var debugDiv = null; function writedbg(msg) {if(debugDiv) debugDiv.value += msg + "\n";} function frgba(r, g, b, a) {return ["rgba(", r, ",", g, ",", b, ",", a, ")"].join("");} function frgb(r, g, b) {return ["rgb(", r, ",", g, ",", b, ")"].join("");} // get a URL parameter by name function getURL_Param(paramName, defVal) { paramName = paramName.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); var regexS = "[\\?&]"+paramName+"=([^&#]*)"; var regex = new RegExp(regexS); var results = regex.exec(window.location.href); if(results == null) return defVal; else return results[1]; } //************************************************************************************************ var KERBIN_R = 600e3 var KERBIN_GM = (9.80665*KERBIN_R*KERBIN_R);// A = M*G/r^2, M*G = A*r^2, A = 9.8665 m/s^2, r = KERBIN_R m var infinity = 1.7976931348623157E+10308; var periapsis = 0, apoapsis = 0; var periapsisVel = 0, apoapsisVel = 0;// these are calculated from the above for elliptical orbits var elliptical = true; var lenScale = 1; var velScale = 1; var zooming = false; var savedScale = 0; var safety = false; //************************************************************************************************ var scale = 2000e3; var canvasMinX = -scale, canvasMinY = -scale; var canvasSizeX = 3*scale, canvasSizeY = 2*scale; var width = 0, height = 0; var scaleX, scaleY; // Screen/world coordinate transforms function s2w_x(x) {return (x/scaleX) + canvasMinX;} function s2w_y(y) {return ((height - y)/scaleY) + canvasMinY;} function w2s_x(x) {return (x - canvasMinX)*scaleX;} function w2s_y(y) {return height - (y - canvasMinY)*scaleY;} function Circle(ctx, x, y, r) { ctx.beginPath(); ctx.arc((x - canvasMinX)*scaleX, (y - canvasMinY)*scaleY, r, 0, 2*pi, false); ctx.stroke(); } function Line(ctx, x, y, x2, y2) { ctx.beginPath(); ctx.moveTo((x - canvasMinX)*scaleX, (y - canvasMinY)*scaleY); ctx.lineTo((x2 - canvasMinX)*scaleX, (y2 - canvasMinY)*scaleY); ctx.stroke(); } function DrawMainCanvas() { var ctx = mainCanvasCtx; ctx.fillStyle = "#EEEEEE"; ctx.fillRect(0, 0, mainCanvas.width, mainCanvas.height); var width = ctx.canvas.width; var height = ctx.canvas.height; var scale = canvasSizeX/(ctx.canvas.width + 1); var aspect = ctx.canvas.width/ctx.canvas.height; ctx.save(); ctx.translate(0, ctx.canvas.height); ctx.scale(1.0, -1.0); ctx.lineWidth = 1; ctx.fillStyle = "#000088"; ctx.strokeStyle = "#000000"; ctx.beginPath(); ctx.arc(w2s_x(0), w2s_y(0), KERBIN_R/scale, 0, 2*pi, false); ctx.fill(); ctx.strokeStyle = "#6666AA"; ctx.beginPath(); ctx.arc(w2s_x(0), w2s_y(0), (KERBIN_R + 35e3)/scale, 0, 2*pi, false); ctx.stroke(); semimaj = (apoapsis + periapsis)/2; semimin = Math.sqrt(apoapsis*periapsis); ctx.strokeStyle = "#000000"; ctx.beginPath(); ctx.moveTo(w2s_x(apoapsis), w2s_y(0)); n = 720 for(j = 0; j <= n; ++j) { th = Math.PI*2.0*j/n; x = w2s_x(Math.cos(th)*semimaj + semimaj - periapsis); y = w2s_y(Math.sin(th)*semimin); ctx.lineTo(Math.min(2*width, Math.max(-width, x)), Math.min(2*height, Math.max(-height, y))); } ctx.stroke(); ctx.restore(); } function SetFromApoapsisPeriapsisEll(peri, apo) { apoapsis = apo; periapsis = peri; // Can not make an elliptical trajectory parabolic/hyperbolic by setting apsides if(apoapsis < periapsis) { tmp = apoapsis; apoapsis = periapsis; periapsis = tmp; } periapsisVel = Math.sqrt(KERBIN_GM*(2.0/periapsis - 2.0/(periapsis + apoapsis))); apoapsisVel = Math.sqrt(KERBIN_GM*(2.0/apoapsis - 2.0/(periapsis + apoapsis))); } function SetFromApoapsis(apo) { if(elliptical) SetFromApoapsisPeriapsisEll(periapsis, apo); else // Parabolic/hyperbolic SetFromApoapsisApovel(); } function SetFromPeriapsis(peri) { if(elliptical) SetFromApoapsisPeriapsisEll(peri, apoapsis); else // Parabolic/hyperbolic SetFromPeriapsisPerivel(); } function SetFromApovelPerivel(apovel, perivel) { // v = sqrt(mu*(2.0/r - 1/a)); // // So: // r1 = 2.0/(2.0/r0 - v0*v0/mu) - r0; // v1 = sqrt(mu*(2.0/r1 - 2.0/(r0 + r1))); // // Wolfram Alpha to the rescue... // If v0*v0 != v0*v1: (v0 != v1, false for circular orbits) // r0 = 2*mu/(v0*v0 - v0*v1) // If v0*(v0 + v1) != 0: (v0 > 0 and v0 + v1 > 0, which is always true for sane numbers) // r0 = 2*mu/(v0*(v0 + v1)) if(apovel > perivel) { tmp = apovel; apovel = periapsisVel; perivel = tmp; } periapsisVel = perivel; apoapsisVel = apovel; periapsis = 2*KERBIN_GM/(perivel*(perivel + apovel)); apoapsis = 2.0/(2.0/periapsis - perivel*perivel/KERBIN_GM) - periapsis; // apoapsisVelCheck = Math.sqrt(KERBIN_GM*(2.0/apoapsis - 2.0/(periapsis + apoapsis))); } function SetFromPeriapsisPerivel(peri, perivel) { periapsis = peri; periapsisVel = perivel; // Can make an elliptical trajectory parabolic/hyperbolic periEscVel = Math.sqrt(2.0*KERBIN_GM/periapsis); if(periapsisVel > periEscVel) { elliptical = false; apoapsis = infinity; // Excess velocity apoapsisVel = Math.sqrt(2.0*((periapsisVel*periapsisVel)/2.0 - KERBIN_GM/periapsis)); } else { elliptical = true; apoapsis = 2.0/(2.0/periapsis - periapsisVel*periapsisVel/KERBIN_GM) - periapsis; apoapsisVel = Math.sqrt(KERBIN_GM*(2.0/apoapsis - 2.0/(periapsis + apoapsis))); } } function SetFromApoapsisApovel(apo, apovel) { apoapsis = apo; apoapsisVel = apovel; // Can make an elliptical trajectory parabolic/hyperbolic, and if so, swaps periapsis/apoapsis // V = sqrt(KERBIN_GM*(2.0/r - 2.0/(peri + apo))) // 2.0/(2.0/apo - V^2/KERBIN_GM) - apo = peri apoEscVel = Math.sqrt(2.0*KERBIN_GM/apoapsis); if(apoapsisVel > apoEscVel) { elliptical = false; periapsis = apoapsis; apoapsis = infinity; // apoapsisVel is excess velocity periapsisVel = apoapsisVel; apoapsisVel = Math.sqrt(2.0*((periapsisVel*periapsisVel)/2.0 - KERBIN_GM/periapsis)); } else { elliptical = true; periapsis = 2.0/(2.0/apoapsis - apoapsisVel*apoapsisVel/KERBIN_GM) - apoapsis; periapsisVel = Math.sqrt(KERBIN_GM*(2.0/periapsis - 2.0/(periapsis + apoapsis))); } } // Taylor series approximation of inverse of Kepler's equation function InvKep(M, e) { // 3! = 6, 5! = 120, 7! = 540, 9! = 362880 me = 1 - e me2 = me*me me3 = me2*me me4 = me2*me2 me7 = me4*me3 me10 = me7*me3 me13 = me10*me3 e2 = e*e e3 = e2*e e4 = e3*e E = M*(1.0/me - M*M*(e/(me4*6) + M*M*((9*e2 + e)/(me7*120) - M*M*((225*e3 + 54*e2 + e)/(me10*540) + M*M*(11025*e4 + 4131*e3 + 243*e2 + e)/(me13*362880))))); return E; } function SetFromDataPoints(data) { // We only have velocity, altitude, // v = sqrt(mu*(2/r - 1/a)) // Given mu, v0, r0, v1, r1: // v0 = sqrt(mu*(2/r0 - 1/a)) // v1 = sqrt(mu*(2/r1 - 1/a)) // r0 = 2/(v0^2/mu + 1/a) // r1 = 2/(v1^2/mu + 1/a) // a = 1/(2/r0 - v0^2/mu) // a = 1/(2/r1 - v1^2/mu) // a = (apo + peri)/2 // E = ((v*v)/2.0 - (G*m)/r) // Period: // P = 2*pi*sqrt(a**3/mu) // Mean anomaly: // M = 2*pi*t/P = t/sqrt(a**3/mu) // M = E - e*sin(E) // t/sqrt(a**3/mu) = E - e*sin(E) // E = InvKep(t/sqrt(a**3/mu), e) // tan(halftheta) = sqrt((1 + e)/(1 - e))*tan(E/2) // r = a*(1 - e*e)/(1 + e*cos(theta)) } // Set up ellipse from altitude, velocity, and angle function SetFromRVA(r, v, ang) { // angle is of velocity vector, tangent to ellipse, to a ray from the planet cosa = Math.cos(ang) sina = Math.sin(ang) //a = 1/(2/r - v^2/mu) a = 1.0/(2.0/r - v*v/KERBIN_GM);// semimajor axis // Only known with e: // b = a*sqrt(1 - e*e) // e = (apo - peri)/(apo + peri) = (apo - peri)/(2*a) // peri = a - e*a // apo = 2*a - peri b = a*Math.sqrt(1 - e*e);// semiminor axis // point and tangent on ellipse by angle from center of corresponding circle: // ex = cos(th), ey = sin(th)*b/a // tx = -sin(th), ty = cos(th)*b/a // atan(-tan(ang)*b/a) = th } function UpdateStats() { debugDiv.value = "" peri = periapsis; apo = apoapsis; lenprec = (lenScale <= 1)? 1 : 4; velprec = (velScale <= 1)? 1 : 4; document.getElementById("periInput").value = ((periapsis - KERBIN_R)/lenScale).toFixed(lenprec); document.getElementById("apoInput").value = ((apoapsis - KERBIN_R)/lenScale).toFixed(lenprec); document.getElementById("perivelInput").value = (periapsisVel/velScale).toFixed(velprec); document.getElementById("apovelInput").value = (apoapsisVel/velScale).toFixed(velprec); if(elliptical) { vperi = periapsisVel; vapo = apoapsisVel; semimaj = (apo + peri)/2; semimin = Math.sqrt(apo*peri); ecc = (1 - 2.0/(apo/peri + 1)); period = 2.0*Math.PI*Math.sqrt(Math.pow((peri + apo)/2.0, 3)/KERBIN_GM) periEscVel = Math.sqrt(2.0*KERBIN_GM/periapsis) apoEscVel = Math.sqrt(2.0*KERBIN_GM/apoapsis) periHohDV = Math.abs(vperi - Math.sqrt(KERBIN_GM/periapsis)) apoHohDV = Math.abs(vapo - Math.sqrt(KERBIN_GM/apoapsis)) writedbg("periapsis: " + (periapsis/1e3).toFixed(4) + " km, apoapsis: " + (apoapsis/1e3).toFixed(4) + " km"); writedbg("periapsis: " + ((periapsis - KERBIN_R)/1e3).toFixed(4) + " km AMSL, " + "apoapsis: " + ((apoapsis - KERBIN_R)/1e3).toFixed(4) + " km AMSL"); writedbg("\nvelocity at periapsis: " + vperi.toFixed(1) + " m/s, " + "at apoapsis: " + vapo.toFixed(1) + " m/s"); writedbg("Hohmann delta-v at periapsis: " + periHohDV.toFixed(1) + " m/s, " + "at apoapsis: " + apoHohDV.toFixed(1) + " m/s"); writedbg("Vesc at periapsis: " + periEscVel.toFixed(1) + " m/s, " + "at apoapsis: " + apoEscVel.toFixed(1) + " m/s"); writedbg("\nperiod: " + (period/60).toFixed(1) + " minutes"); writedbg("semimajor axis: " + (semimaj/1e3).toFixed(4) + " km"); writedbg("semiminor axis: " + (semimin/1e3).toFixed(4) + " km"); writedbg("eccentricity: " + ecc.toFixed(1)); } else { writedbg("periapsis: " + (periapsis/1e3).toFixed(4) + " km"); writedbg("periapsis: " + ((periapsis - KERBIN_R)/1e3).toFixed(4) + " km AMSL"); writedbg("velocity at periapsis: " + periapsisVel.toFixed(1) + " m/s, excess velocity: " + apoapsisVel.toFixed(1) + " m/s"); } var plotLink = document.getElementById("plotLink"); plotLink.href = "http://files.arklyffe.com/orbcalc.html?" + "a=" + (""+apoapsis) + "&p=" + (""+periapsis) + "&av=" + (""+apoapsisVel) + "&pv=" + (""+periapsisVel); } function PeriapsisChanged() { paramLock = document.getElementById("paramLockSelect").value; peri = (+document.getElementById("periInput").value + KERBIN_R)*lenScale; if(paramLock == "apo-peri") SetFromPeriapsis(peri); else SetFromPeriapsisPerivel(peri, periapsisVel); UpdateStats(); DrawMainCanvas(); } function ApsoapsisChanged() { paramLock = document.getElementById("paramLockSelect").value; apo = (+document.getElementById("apoInput").value + KERBIN_R)*lenScale; if(paramLock == "apo-peri") SetFromApoapsis(apo); else SetFromApoapsisApovel(apo, apoapsisVel); UpdateStats(); DrawMainCanvas(); } // Set velocity at apoapsis, recompute periapsis function ApoVelChanged() { paramLock = document.getElementById("paramLockSelect").value; apovel = +document.getElementById("apovelInput").value*velScale; if(paramLock == "apo-peri") SetFromApovelPerivel(apovel, periapsisVel); else SetFromApoapsisApovel(apoapsis, apovel); UpdateStats(); DrawMainCanvas(); } // Set velocity at periapsis, recompute apoapsis function PeriVelChanged() { paramLock = document.getElementById("paramLockSelect").value; perivel = +document.getElementById("perivelInput").value*velScale; if(paramLock == "apo-peri") SetFromApovelPerivel(apoapsisVel, perivel); else SetFromPeriapsisPerivel(periapsis, perivel); UpdateStats(); DrawMainCanvas(); } function ZoomIn() { scale /= 2; canvasMinX = -scale; canvasMinY = -scale; canvasSizeX = 3*scale; canvasSizeY = canvasSizeX*height/width;// square aspect ratio scaleX = width/canvasSizeX; scaleY = height/canvasSizeY; DrawMainCanvas(); } function ZoomOut() { scale *= 2; canvasMinX = -scale; canvasMinY = -scale; canvasSizeX = 3*scale; canvasSizeY = canvasSizeX*height/width;// square aspect ratio scaleX = width/canvasSizeX; scaleY = height/canvasSizeY; DrawMainCanvas(); } function MouseMoved(evt, x, y) {} function MouseDown(evt, x, y) { if(safety) return; if(event.shiftKey) { zooming = true; savedScale = scale; } else { zooming = false; MouseDragging(evt, x, y, x, y); } } function MouseClicked(evt, x, y) {} function MouseDragged(evt, x, y, prevX, prevY) {} function MouseDragging(evt, x, y, prevX, prevY) { if(safety) return; if(zooming) { width = mainCanvasCtx.canvas.width; height = mainCanvasCtx.canvas.height; // Zoom scale = savedScale + (savedScale*(y - cursorClickY))/height; canvasMinX = -scale; canvasMinY = -scale; canvasSizeX = 3*scale; canvasSizeY = canvasSizeX*height/width;// square aspect ratio scaleX = width/canvasSizeX; scaleY = height/canvasSizeY; DrawMainCanvas(); } else { MouseDragAltitude(evt, x, y, prevX, prevY) } } function MouseDragAltitude(evt, x, y, prevX, prevY) { if(safety) return; paramLock = document.getElementById("paramLockSelect").value; click_wx = s2w_x(cursorClickX) wx = s2w_x(x) if(click_wx > 0) { if(wx > 0) { elliptical = true; if(paramLock == "apo-peri") { if(wx < periapsis) wx = periapsis; SetFromApoapsis(wx); } else { apoapsis = savedApoapsis; periapsis = savedPeriapsis; apoapsisVel = savedApovel; periapsisVel = savedPerivel; SetFromApoapsisApovel(wx, apoapsisVel); } } } else { wx = -wx; if(wx > 0) { if(paramLock == "apo-peri") { if(wx > apoapsis) wx = apoapsis; SetFromPeriapsis(wx); } else { apoapsis = savedApoapsis; periapsis = savedPeriapsis; apoapsisVel = savedApovel; periapsisVel = savedPerivel; SetFromPeriapsisPerivel(wx, periapsisVel); } } } UpdateStats(); DrawMainCanvas(); } function UnitsChanged(self) { units = document.getElementById("unitsSelect").value; if(units == "m_ms") { lenScale = 1; velScale = 1; } else if(units == "km_ms") { lenScale = 1000; velScale = 1; } else if(units == "m_kms") { lenScale = 1; velScale = 1000; } else if(units == "km_kms") { lenScale = 1000; velScale = 1000; } else { lenScale = 1; velScale = 1; } UpdateStats(); } function SafetyBtnChanged(self) { safety = document.getElementById("safetyBtn").checked; } //************************************************************************************************ window.onload = function() { apo = +getURL_Param("a", 0) peri = +getURL_Param("p", 0) apoV = +getURL_Param("av", 0) periV = +getURL_Param("pv", 0) if(apo > 0 && peri > 0) SetFromApoapsisPeriapsisEll(apo, peri); else if(apo > 0 && apoV > 0) SetFromApoapsisApovel(apo, apoV); else if(peri > 0 && periV > 0) SetFromPeriapsisPerivel(peri, periV); else SetFromApoapsisPeriapsisEll(50e3 + KERBIN_R, 200e3 + KERBIN_R) debugDiv = document.getElementById("debugDiv"); mainCanvas = document.getElementById("mainCanvas"); mainCanvasCtx = mainCanvas.getContext("2d"); width = mainCanvasCtx.canvas.width; height = mainCanvasCtx.canvas.height; canvasSizeY = canvasSizeX*height/width;//square the initial aspect scaleX = width/canvasSizeX; scaleY = height/canvasSizeY; //Mouse events mainCanvas.onmouseover = function(evt) {document.body.style.cursor = "crosshair"; return false;} mainCanvas.onmouseout = function(evt) {document.body.style.cursor = "default"; return false;} mainCanvas.onmousedown = function(evt) { mouseIsDown = true; cursorLastX = evt.clientX - mainCanvas.offsetLeft + (window.pageXOffset || document.body.scrollLeft || document.documentElement.scrollLeft); cursorLastY = evt.clientY - mainCanvas.offsetTop + (window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop); cursorClickX = cursorLastX; cursorClickY = cursorLastY; MouseDown(evt, cursorClickX, cursorClickY); return false; } //if mouse is down, get mouseup events from window, not just canvas. Otherwise ignore. onmouseup = function(evt) { if(mouseIsDown) { var x = evt.clientX - mainCanvas.offsetLeft + (window.pageXOffset || document.body.scrollLeft || document.documentElement.scrollLeft); var y = evt.clientY - mainCanvas.offsetTop + (window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop); mouseIsDown = false; if(mouseIsDragging) { //mouse dragged mouseIsDragging = false; MouseDragged(evt, x, y, cursorLastX, cursorLastY); } else { //mouse clicked MouseClicked(evt, x, y); } } return false; } onmousemove = function(evt) { var x = evt.clientX - mainCanvas.offsetLeft + (window.pageXOffset || document.body.scrollLeft || document.documentElement.scrollLeft); var y = evt.clientY - mainCanvas.offsetTop + (window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop); if(mouseIsDown) { mouseIsDragging = true; MouseDragging(evt, x, y, cursorLastX, cursorLastY); cursorLastX = x; cursorLastY = y; } MouseMoved(evt, x, y); return false; } writedbg("Init done"); UpdateStats(); DrawMainCanvas(); } //************************************************************************************************ </script> </head> <body> <a id="plotLink" href="http://files.arklyffe.com/orbcalc.html">This Orbit</a><br> <canvas id="mainCanvas" width="800" height="600"></canvas><br> <table> <tr> <td>Periapsis AMSL: <input type="text" id="periInput" size="8" onchange="PeriapsisChanged()"></td> <td>Velocity: <input type="text" id="perivelInput" size="8" onchange="PeriVelChanged()"></td> <td>Parameters: <select id="paramLockSelect"> <option value="apo-peri" selected>Apoapsis-Periapsis</option> <option value="aps-vel">Apside-Velocity</option> </select></td> <td><input type="button" value="Zoom -" onclick="ZoomOut()"> <input type="button" value="Zoom +" onclick="ZoomIn()"></td> </tr> <tr> <td>Apoapsis AMSL: <input type="text" id="apoInput" size="8" onchange="ApsoapsisChanged()"></td> <td>Velocity: <input type="text" id="apovelInput" size="8" onchange="ApoVelChanged()"></td> <td>Units: <select id="unitsSelect" onchange="UnitsChanged()"> <option value="m_ms" selected>m, m/s</option> <option value="km_ms">km, m/s</option> <option value="m_kms">m, km/s</option> <option value="km_kms">km, km/s</option> </select></td> <td><input type="checkbox" id="safetyBtn" size="8" onchange="SafetyBtnChanged()">Ignore Mouse</td> </tr> </table> <div><textarea id="debugDiv" rows="20" cols="60" wrap="virtual" style="width:640px; height:250px"></textarea></div><br> </body> </html>