<div class="controls">
<label>Step size h = <span id="h-val">0.3</span>
<input type="range" id="h-slider" min="1" max="20" value="6" step="1">
</label>
<button id="run-btn" type="button">Run one orbit</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<canvas id="cvs" width="440" height="300"></canvas>
<div class="legend">
<span class="dot euler"></span> Euler
<span class="dot rk4"></span> RK4
<span class="dot exact"></span> Exact
</div>
<div id="errors" class="errors"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .9rem; }
label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 120px; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 8px; background: #f7f9fb; }
.legend { font-size: .82rem; margin: .4rem 0; display: flex; gap: 1rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: middle; }
.dot.euler { background: #e63946; }
.dot.rk4 { background: #2a9d8f; }
.dot.exact { background: #457b9d; }
.errors { font-size: .85rem; color: #333; min-height: 2.4em; }
.errors span { margin-right: 1rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Circular orbit: x'' = -x/r^3, y'' = -y/r^3 (GM=1, so circular at r=1 with v=1).
// State vector: [x, y, vx, vy]
// Derivatives: [vx, vy, -x/r^3, -y/r^3]
function deriv(s) {
const r3 = Math.pow(s[0]*s[0] + s[1]*s[1], 1.5);
return [s[2], s[3], -s[0]/r3, -s[1]/r3];
}
function eulerStep(s, h) {
const d = deriv(s);
return s.map((v, i) => v + h * d[i]);
}
function rk4Step(s, h) {
const k1 = deriv(s);
const s2 = s.map((v, i) => v + 0.5 * h * k1[i]);
const k2 = deriv(s2);
const s3 = s.map((v, i) => v + 0.5 * h * k2[i]);
const k3 = deriv(s3);
const s4 = s.map((v, i) => v + h * k3[i]);
const k4 = deriv(s4);
return s.map((v, i) => v + (h / 6) * (k1[i] + 2*k2[i] + 2*k3[i] + k4[i]));
}
function integrateOrbit(stepFn, h) {
const T = 2 * Math.PI; // one full orbit
const steps = Math.ceil(T / h);
const pts = [];
let s = [1, 0, 0, 1]; // start: position (1,0), velocity (0,1)
pts.push([s[0], s[1]]);
for (let i = 0; i < steps; i++) {
s = stepFn(s, h);
pts.push([s[0], s[1]]);
}
return { pts, final: s };
}
function exactPos(t) {
return [Math.cos(t), Math.sin(t)];
}
const cvs = document.getElementById('cvs');
const ctx = cvs.getContext('2d');
const W = cvs.width, H = cvs.height;
const cx = W / 2, cy = H / 2;
const scale = 110; // pixels per unit
function toCanvas(x, y) {
return [cx + x * scale, cy - y * scale];
}
function drawPath(pts, color, lineWidth) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
const [x0, y0] = toCanvas(pts[0][0], pts[0][1]);
ctx.moveTo(x0, y0);
for (let i = 1; i < pts.length; i++) {
const [xi, yi] = toCanvas(pts[i][0], pts[i][1]);
ctx.lineTo(xi, yi);
}
ctx.stroke();
}
function drawDot(x, y, color, r) {
const [cx2, cy2] = toCanvas(x, y);
ctx.beginPath();
ctx.arc(cx2, cy2, r, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
}
function drawExactCircle() {
const [ex, ey] = toCanvas(0, 0);
ctx.beginPath();
ctx.arc(ex, ey, scale, 0, 2 * Math.PI);
ctx.strokeStyle = '#457b9d';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
function getH() {
const slider = document.getElementById('h-slider');
// map slider 1..20 -> h 0.05..1.5
return 0.05 + (parseInt(slider.value) - 1) * (1.45 / 19);
}
function run() {
const h = getH();
const euler = integrateOrbit(eulerStep, h);
const rk = integrateOrbit(rk4Step, h);
const T = 2 * Math.PI;
const [ex, ey] = exactPos(T); // = (1,0) after full period
// Errors: distance from exact position after one orbit
const eulerErr = Math.hypot(euler.final[0] - ex, euler.final[1] - ey);
const rkErr = Math.hypot(rk.final[0] - ex, rk.final[1] - ey);
ctx.clearRect(0, 0, W, H);
drawExactCircle();
drawPath(euler.pts, '#e63946', 1.8);
drawPath(rk.pts, '#2a9d8f', 1.8);
// Draw planet dots at final position
drawDot(euler.final[0], euler.final[1], '#e63946', 5);
drawDot(rk.final[0], rk.final[1], '#2a9d8f', 5);
// Draw sun at origin
drawDot(0, 0, '#f4a261', 7);
// Log for node verification
console.log(fmt('h=%h% euler_err=%eulerErr% rk4_err=%rkErr%', { h: h.toFixed(3), eulerErr: eulerErr.toFixed(6), rkErr: rkErr.toFixed(6) }));
const errDiv = document.getElementById('errors');
errDiv.innerHTML =
fmt('<span><b style="color:#e63946">Euler</b> error: %eulerErr%</span><span><b style="color:#2a9d8f">RK4</b> error: %rkErr%</span>', { eulerErr: eulerErr.toFixed(4), rkErr: rkErr.toFixed(4) });
}
const slider = document.getElementById('h-slider');
const hVal = document.getElementById('h-val');
slider.addEventListener('input', () => {
hVal.textContent = getH().toFixed(2);
});
document.getElementById('run-btn').addEventListener('click', run);
document.getElementById('reset-btn').addEventListener('click', () => {
ctx.clearRect(0, 0, W, H);
document.getElementById('errors').innerHTML = '';
});
// Initial draw
hVal.textContent = getH().toFixed(2);
run();
<div class="controls">
<label>Step size h = <span id="h-val">0.3</span>
<input type="range" id="h-slider" min="1" max="20" value="6" step="1">
</label>
<button id="run-btn" type="button">Run one orbit</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<canvas id="cvs" width="440" height="300"></canvas>
<div class="legend">
<span class="dot euler"></span> Euler
<span class="dot rk4"></span> RK4
<span class="dot exact"></span> Exact
</div>
<div id="errors" class="errors"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .9rem; }
label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 120px; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 8px; background: #f7f9fb; }
.legend { font-size: .82rem; margin: .4rem 0; display: flex; gap: 1rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: middle; }
.dot.euler { background: #e63946; }
.dot.rk4 { background: #2a9d8f; }
.dot.exact { background: #457b9d; }
.errors { font-size: .85rem; color: #333; min-height: 2.4em; }
.errors span { margin-right: 1rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Circular orbit: x'' = -x/r^3, y'' = -y/r^3 (GM=1, so circular at r=1 with v=1).
// State vector: [x, y, vx, vy]
// Derivatives: [vx, vy, -x/r^3, -y/r^3]
function deriv(s) {
const r3 = Math.pow(s[0]*s[0] + s[1]*s[1], 1.5);
return [s[2], s[3], -s[0]/r3, -s[1]/r3];
}
function eulerStep(s, h) {
const d = deriv(s);
return s.map((v, i) => v + h * d[i]);
}
function rk4Step(s, h) {
const k1 = deriv(s);
const s2 = s.map((v, i) => v + 0.5 * h * k1[i]);
const k2 = deriv(s2);
const s3 = s.map((v, i) => v + 0.5 * h * k2[i]);
const k3 = deriv(s3);
const s4 = s.map((v, i) => v + h * k3[i]);
const k4 = deriv(s4);
return s.map((v, i) => v + (h / 6) * (k1[i] + 2*k2[i] + 2*k3[i] + k4[i]));
}
function integrateOrbit(stepFn, h) {
const T = 2 * Math.PI; // one full orbit
const steps = Math.ceil(T / h);
const pts = [];
let s = [1, 0, 0, 1]; // start: position (1,0), velocity (0,1)
pts.push([s[0], s[1]]);
for (let i = 0; i < steps; i++) {
s = stepFn(s, h);
pts.push([s[0], s[1]]);
}
return { pts, final: s };
}
function exactPos(t) {
return [Math.cos(t), Math.sin(t)];
}
const cvs = document.getElementById('cvs');
const ctx = cvs.getContext('2d');
const W = cvs.width, H = cvs.height;
const cx = W / 2, cy = H / 2;
const scale = 110; // pixels per unit
function toCanvas(x, y) {
return [cx + x * scale, cy - y * scale];
}
function drawPath(pts, color, lineWidth) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
const [x0, y0] = toCanvas(pts[0][0], pts[0][1]);
ctx.moveTo(x0, y0);
for (let i = 1; i < pts.length; i++) {
const [xi, yi] = toCanvas(pts[i][0], pts[i][1]);
ctx.lineTo(xi, yi);
}
ctx.stroke();
}
function drawDot(x, y, color, r) {
const [cx2, cy2] = toCanvas(x, y);
ctx.beginPath();
ctx.arc(cx2, cy2, r, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
}
function drawExactCircle() {
const [ex, ey] = toCanvas(0, 0);
ctx.beginPath();
ctx.arc(ex, ey, scale, 0, 2 * Math.PI);
ctx.strokeStyle = '#457b9d';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
function getH() {
const slider = document.getElementById('h-slider');
// map slider 1..20 -> h 0.05..1.5
return 0.05 + (parseInt(slider.value) - 1) * (1.45 / 19);
}
function run() {
const h = getH();
const euler = integrateOrbit(eulerStep, h);
const rk = integrateOrbit(rk4Step, h);
const T = 2 * Math.PI;
const [ex, ey] = exactPos(T); // = (1,0) after full period
// Errors: distance from exact position after one orbit
const eulerErr = Math.hypot(euler.final[0] - ex, euler.final[1] - ey);
const rkErr = Math.hypot(rk.final[0] - ex, rk.final[1] - ey);
ctx.clearRect(0, 0, W, H);
drawExactCircle();
drawPath(euler.pts, '#e63946', 1.8);
drawPath(rk.pts, '#2a9d8f', 1.8);
// Draw planet dots at final position
drawDot(euler.final[0], euler.final[1], '#e63946', 5);
drawDot(rk.final[0], rk.final[1], '#2a9d8f', 5);
// Draw sun at origin
drawDot(0, 0, '#f4a261', 7);
// Log for node verification
console.log(fmt('h=%h% euler_err=%eulerErr% rk4_err=%rkErr%', { h: h.toFixed(3), eulerErr: eulerErr.toFixed(6), rkErr: rkErr.toFixed(6) }));
const errDiv = document.getElementById('errors');
errDiv.innerHTML =
fmt('<span><b style="color:#e63946">Euler</b> error: %eulerErr%</span><span><b style="color:#2a9d8f">RK4</b> error: %rkErr%</span>', { eulerErr: eulerErr.toFixed(4), rkErr: rkErr.toFixed(4) });
}
const slider = document.getElementById('h-slider');
const hVal = document.getElementById('h-val');
slider.addEventListener('input', () => {
hVal.textContent = getH().toFixed(2);
});
document.getElementById('run-btn').addEventListener('click', run);
document.getElementById('reset-btn').addEventListener('click', () => {
ctx.clearRect(0, 0, W, H);
document.getElementById('errors').innerHTML = '';
});
// Initial draw
hVal.textContent = getH().toFixed(2);
run();
Comments
Loading comments...