(
);
const Pause = ({ size = 24 }) => (
);
const RotateCcw = ({ size = 24 }) => (
);
const Rocket = ({ size = 24 }) => (
);
const ZoomIn = ({ size = 24 }) => (
);
const ZoomOut = ({ size = 24 }) => (
);
const OrbitalMechanics = () => {
const [m1, setM1] = useState(500);
const [m2, setM2] = useState(5);
const [m3, setM3] = useState(0);
const [m3angle, setM3angle] = useState(0);
const [m4, setM4] = useState(0);
const [m4angle, setM4angle] = useState(180);
const [m2angle, setM2angle] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [revolutionSpeed, setRevolutionSpeed] = useState(0.5);
const [zoom, setZoom] = useState(1);
const [pan, setPan] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 });
const [showOutputPanel, setShowOutputPanel] = useState(true);
const [activeTab, setActiveTab] = useState(‘system’);
const [missionSubTab, setMissionSubTab] = useState(‘jwst’);
const [trajectoryType, setTrajectoryType] = useState(‘direct’);
const [destination, setDestination] = useState(‘L2’);
const [spacecraftActive, setSpacecraftActive] = useState(false);
const [spacecraftProgress, setSpacecraftProgress] = useState(0);
const [spacecraftPosition, setSpacecraftPosition] = useState({ x: 0, y: 0 });
const [lucyActive, setLucyActive] = useState(false);
const [lucyPhase, setLucyPhase] = useState(1);
const [lucyProgress, setLucyProgress] = useState(0);
const [lucyPosition, setLucyPosition] = useState({ x: 0, y: 0 });
const [lucyPrevPosition, setLucyPrevPosition] = useState({ x: 0, y: 0 });
const [lucyTrail, setLucyTrail] = useState([]);
const [lastLucyAngle, setLastLucyAngle] = useState(0);
const [fullMissionMode, setFullMissionMode] = useState(false);
const [startConfigs, setStartConfigs] = useState({});
const [lucySpeed, setLucySpeed] = useState(0.5);
const canvasRef = useRef(null);
const canvasContainerRef = useRef(null);
const newtonSolver = (f, x0, tol, maxIter) => {
const tolerance = tol || 1e-6;
const maxIterations = maxIter || 50;
let x = x0;
for (let i = 0; i < maxIterations; i++) {
const fx = f(x);
if (Math.abs(fx) < tolerance) return x;
const dx = 1e-6;
const fprime = (f(x + dx) - f(x - dx)) / (2 * dx);
if (fprime === 0) return x;
x -= fx / fprime;
}
return x;
};
const d12 = Math.pow(m2 / m1, 1/3) * 400 + 200;
const d14 = Math.pow(m4 / m1, 1/3) * 600 + 400;
const d23 = Math.pow(m3 / m2, 1/3) * 80 + 40;
const mu12 = m2 / (m1 + m2);
const mu14 = m4 / (m1 + m4);
const mu23 = m3 / (m2 + m3);
const posM1 = { x: 0, y: 0 };
const posM2 = { x: d12, y: 0 };
const m4_absolute_angle = m4angle * Math.PI / 180;
const m4_relative_angle = m4_absolute_angle - m2angle * Math.PI / 180;
const posM4 = {
x: d14 * Math.cos(m4_relative_angle),
y: d14 * Math.sin(m4_relative_angle)
};
const posM3 = {
x: posM2.x + d23 * Math.cos(m3angle * Math.PI / 180),
y: posM2.y + d23 * Math.sin(m3angle * Math.PI / 180)
};
const d13 = Math.sqrt(
Math.pow(posM3.x - posM1.x, 2) + Math.pow(posM3.y - posM1.y, 2)
);
const d3jwst = spacecraftActive ? Math.sqrt(
Math.pow(spacecraftPosition.x - posM3.x, 2) + Math.pow(spacecraftPosition.y - posM3.y, 2)
) : 0;
const d2lucy = lucyActive ? Math.sqrt(
Math.pow(lucyPosition.x - posM2.x, 2) + Math.pow(lucyPosition.y - posM2.y, 2)
) : 0;
const d4lucy = lucyActive && m4 > 0 ? Math.sqrt(
Math.pow(lucyPosition.x – posM4.x, 2) + Math.pow(lucyPosition.y – posM4.y, 2)
) : 0;
const barycenterM2M3 = {
x: (m2 * posM2.x + m3 * posM3.x) / (m2 + m3),
y: (m2 * posM2.y + m3 * posM3.y) / (m2 + m3)
};
const computeLagrangianPoints = (mu, distance, shift, angle) => {
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
const x1_norm = -mu;
const x2_norm = 1 – mu;
const f = (x) => {
const r1 = Math.abs(x – x1_norm);
const r2 = Math.abs(x – x2_norm);
const term1 = (1 – mu) * (x – x1_norm) / (r1 * r1 * r1 || 1);
const term2 = mu * (x – x2_norm) / (r2 * r2 * r2 || 1);
return x – term1 – term2;
};
const approx = Math.pow(mu / 3, 1/3);
const approx3 = 5 * mu / 12;
const l1x = newtonSolver(f, x2_norm – approx) * distance;
const l2x = newtonSolver(f, x2_norm + approx) * distance;
const l3x = newtonSolver(f, x1_norm – (1 + approx3)) * distance;
const l4x = (0.5 – mu) * distance;
const l4y = Math.sqrt(3) / 2 * distance;
const l5x = (0.5 – mu) * distance;
const l5y = -Math.sqrt(3) / 2 * distance;
const rotate = (x, y) => ({
x: x * cosAngle – y * sinAngle + shift.x,
y: x * sinAngle + y * cosAngle + shift.y
});
return {
l1: rotate(l1x, 0),
l2: rotate(l2x, 0),
l3: rotate(l3x, 0),
l4: rotate(l4x, l4y),
l5: rotate(l5x, l5y)
};
};
const d12_barycentric = Math.sqrt(
barycenterM2M3.x * barycenterM2M3.x +
barycenterM2M3.y * barycenterM2M3.y
);
const angle12_barycentric = Math.atan2(barycenterM2M3.y, barycenterM2M3.x);
const mu12_barycentric = (m2 + m3) / (m1 + m2 + m3);
const cm1_23 = {
x: barycenterM2M3.x * mu12_barycentric,
y: barycenterM2M3.y * mu12_barycentric
};
const lPoints_m1m2 = computeLagrangianPoints(mu12_barycentric, d12_barycentric, cm1_23, angle12_barycentric);
const cm2_3 = {
x: posM2.x + (posM3.x – posM2.x) * mu23,
y: posM2.y + (posM3.y – posM2.y) * mu23
};
const lPoints_m2m3 = computeLagrangianPoints(mu23, d23, cm2_3, m3angle * Math.PI / 180);
const cm1_4 = { x: posM4.x * mu14, y: posM4.y * mu14 };
const lPoints_m1m4 = computeLagrangianPoints(mu14, d14, cm1_4, m4_relative_angle);
const getDestinationPoint = () => {
const destMap = {
‘L1’: lPoints_m1m2.l1,
‘L2’: lPoints_m1m2.l2,
‘L3’: lPoints_m1m2.l3,
‘L4’: lPoints_m1m2.l4,
‘L5’: lPoints_m1m2.l5
};
return destMap[destination];
};
const computeTrajectory = (progress) => {
const start = posM2;
const end = getDestinationPoint();
if (trajectoryType === ‘direct’) {
const t = progress;
const easedT = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
return {
x: start.x + (end.x - start.x) * easedT,
y: start.y + (end.y - start.y) * easedT
};
} else if (trajectoryType === 'hohmann') {
const t = progress;
const dx = end.x - start.x;
const dy = end.y - start.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const perpX = -Math.sin(angle);
const perpY = Math.cos(angle);
const curveAmount = dist * 0.3;
const baseX = start.x + dx * t;
const baseY = start.y + dy * t;
const offset = Math.sin(t * Math.PI) * curveAmount;
return {
x: baseX + perpX * offset,
y: baseY + perpY * offset
};
} else if (trajectoryType === 'spiral') {
const t = progress;
const spirals = 2;
const angle = t * Math.PI * 2 * spirals;
const directX = start.x + (end.x - start.x) * t;
const directY = start.y + (end.y - start.y) * t;
const dx = end.x - start.x;
const dy = end.y - start.y;
const spiralRadius = Math.sqrt(dx * dx + dy * dy) * 0.1 * (1 - t);
return {
x: directX + Math.cos(angle) * spiralRadius,
y: directY + Math.sin(angle) * spiralRadius
};
} else if (trajectoryType === 'gravity') {
const t = progress;
if (m3 > 0 && t < 0.5) {
const midT = t * 2;
return {
x: start.x + (posM3.x - start.x) * midT,
y: start.y + (posM3.y - start.y) * midT
};
} else {
const midT = m3 > 0 ? (t – 0.5) * 2 : t;
const midPoint = m3 > 0 ? posM3 : start;
return {
x: midPoint.x + (end.x – midPoint.x) * midT,
y: midPoint.y + (end.y – midPoint.y) * midT
};
}
}
return start;
};
const computeLucyTrajectory = (phase, progress) => {
const oR1 = 50, oR2 = 70, oR3 = 90, sR = 60;
const config = startConfigs[phase] || {};
const startPos = config.startPos || null;
if (phase === 1) {
const t = progress;
const r = oR1 * t;
const spiralAngle = t * Math.PI * 0.5;
return { x: posM2.x + r * Math.cos(spiralAngle), y: posM2.y + r * Math.sin(spiralAngle) };
} else if (phase === 2 || phase === 3 || phase === 7) {
const center = posM2;
const targetR = phase === 2 ? oR1 : phase === 3 ? oR2 : oR3;
let initialR = targetR, startAngle = 0;
if (startPos) {
startAngle = Math.atan2(startPos.y – center.y, startPos.x – center.x);
initialR = Math.sqrt(Math.pow(startPos.x – center.x, 2) + Math.pow(startPos.y – center.y, 2));
}
const transDuration = Math.abs(initialR – targetR) > 0.01 ? 0.2 : 0;
let r, a;
if (progress < transDuration) {
const t = progress / transDuration;
r = initialR + (targetR - initialR) * t;
a = startAngle;
} else {
const t = (progress - transDuration) / (1 - transDuration);
r = targetR;
a = startAngle + t * Math.PI * 2;
}
return { x: center.x + r * Math.cos(a), y: center.y + r * Math.sin(a) };
} else if (phase === 5 || phase === 9) {
const center = phase === 5 ? lPoints_m1m4.l4 : lPoints_m1m4.l5;
const targetR = sR;
let initialR = targetR, startAngle = 0;
if (startPos) {
startAngle = Math.atan2(startPos.y - center.y, startPos.x - center.x);
initialR = Math.sqrt(Math.pow(startPos.x - center.x, 2) + Math.pow(startPos.y - center.y, 2));
}
const transDuration = Math.abs(initialR - targetR) > 0.01 ? 0.2 : 0;
let r, a;
if (progress < transDuration) {
const t = progress / transDuration;
r = initialR + (targetR - initialR) * t;
a = startAngle + t * Math.PI * 2;
} else {
const t = (progress - transDuration) / (1 - transDuration);
r = targetR;
a = startAngle + transDuration * 2 * Math.PI + t * Math.PI * 2;
}
return { x: center.x + r * Math.cos(a), y: center.y + r * Math.sin(a) };
} else if (phase === 4 || phase === 8) {
if (!m4) return posM2;
const st = startPos || posM2;
const c = phase === 4 ? lPoints_m1m4.l4 : lPoints_m1m4.l5;
const dx = c.x - st.x, dy = c.y - st.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 1e-6) return st;
const en = { x: c.x - (dx / dist) * sR, y: c.y - (dy / dist) * sR };
const t = progress;
const eT = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
return { x: st.x + (en.x - st.x) * eT, y: st.y + (en.y - st.y) * eT };
} else if (phase === 6) {
if (!m4) return posM2;
const st = startPos || lPoints_m1m4.l4;
const dx = posM2.x - st.x, dy = posM2.y - st.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 1e-6) return st;
const en = { x: posM2.x - (dx / dist) * oR3, y: posM2.y - (dy / dist) * oR3 };
const t = progress;
const eT = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
return { x: st.x + (en.x - st.x) * eT, y: st.y + (en.y - st.y) * eT };
}
return posM2;
};
const launchSpacecraft = () => {
setSpacecraftActive(true);
setSpacecraftProgress(0);
};
const resetSpacecraft = () => {
setSpacecraftActive(false);
setSpacecraftProgress(0);
};
const launchLucy = () => {
setLucyActive(true);
setLucyPhase(1);
setLucyProgress(0);
setLucyTrail([]);
setStartConfigs({});
setLucyPrevPosition({ x: 0, y: 0 });
setLastLucyAngle(0);
setFullMissionMode(false);
};
const launchFullLucy = () => {
setLucyActive(true);
setLucyPhase(1);
setLucyProgress(0);
setLucyTrail([]);
setStartConfigs({});
setLucyPrevPosition({ x: 0, y: 0 });
setLastLucyAngle(0);
setFullMissionMode(true);
};
const resetLucy = () => {
setLucyActive(false);
setLucyPhase(1);
setLucyProgress(0);
setLucyTrail([]);
setStartConfigs({});
setLucyPrevPosition({ x: 0, y: 0 });
setLastLucyAngle(0);
setFullMissionMode(false);
};
const continueToNextPhase = () => {
const nextPhase = lucyPhase + 1;
if (nextPhase <= 9) {
const currentPos = computeLucyTrajectory(lucyPhase, 1);
setStartConfigs(prev => ({ …prev, [nextPhase]: { startPos: { …currentPos } } }));
setLucyPhase(nextPhase);
setLucyProgress(0);
}
};
useEffect(() => {
if (spacecraftActive && spacecraftProgress < 1) {
const interval = setInterval(() => {
setSpacecraftProgress(p => {
const newProgress = p + 0.005;
if (newProgress >= 1) {
return 1;
}
return newProgress;
});
}, 50);
return () => clearInterval(interval);
}
}, [spacecraftActive, spacecraftProgress]);
useEffect(() => {
if (lucyActive && lucyProgress < 1) {
const interval = setInterval(() => {
setLucyProgress(p => {
const baseSpeed = 0.009;
const speedMultiplier = lucySpeed;
const newProgress = p + baseSpeed * speedMultiplier;
if (newProgress >= 1) {
return 1;
}
return newProgress;
});
}, 50);
return () => clearInterval(interval);
}
}, [lucyActive, lucyProgress, lucySpeed]);
useEffect(() => {
if (spacecraftActive) {
setSpacecraftPosition(computeTrajectory(spacecraftProgress));
}
}, [spacecraftActive, spacecraftProgress, trajectoryType, destination, posM2, posM3, lPoints_m1m2]);
useEffect(() => {
if (lucyActive) {
const pos = computeLucyTrajectory(lucyPhase, lucyProgress);
setLucyPrevPosition(lucyPosition);
setLucyPosition(pos);
if (lucyProgress % 0.05 < 0.01) {
setLucyTrail(prev => […prev, { …pos, phase: lucyPhase }]);
}
}
}, [lucyActive, lucyPhase, lucyProgress]);
useEffect(() => {
if (lucyActive && lucyProgress >= 1 && fullMissionMode && lucyPhase < 9) {
continueToNextPhase();
}
}, [lucyProgress, lucyPhase, lucyActive, fullMissionMode]);
useEffect(() => {
if (!isPlaying) return;
const interval = setInterval(() => {
setM2angle(a => a + 1 * revolutionSpeed);
if (m3 > 0) setM3angle(a => a + 1.5 * revolutionSpeed);
if (m4 > 0) setM4angle(a => a + 0.15 * revolutionSpeed);
}, 50);
return () => clearInterval(interval);
}, [isPlaying, m3, m4, revolutionSpeed]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext(‘2d’);
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
ctx.fillStyle = ‘#ffffff’;
ctx.fillRect(0, 0, width, height);
const extentX = Math.max(Math.abs(lPoints_m1m2.l3.x), lPoints_m1m2.l2.x, posM2.x, Math.abs(posM3.x), Math.abs(posM4.x)) * 2.2;
const extentY = Math.max(Math.abs(lPoints_m1m2.l4.y), Math.abs(lPoints_m1m2.l5.y), Math.abs(posM3.y), Math.abs(posM4.y)) * 2.2;
const baseScale = Math.min(width / extentX, height / extentY);
const scale = baseScale * zoom;
ctx.save();
ctx.translate(centerX + pan.x * scale, centerY + pan.y * scale);
ctx.scale(scale, -scale);
const gridStep = 100;
const minX = -extentX / zoom;
const maxX = extentX / zoom;
const minY = -extentY / zoom;
const maxY = extentY / zoom;
const gridMinX = Math.floor(minX / gridStep) * gridStep;
const gridMaxX = Math.ceil(maxX / gridStep) * gridStep;
const gridMinY = Math.floor(minY / gridStep) * gridStep;
const gridMaxY = Math.ceil(maxY / gridStep) * gridStep;
ctx.strokeStyle = ‘#d3d3d3’;
ctx.lineWidth = 1 / scale;
for (let x = gridMinX; x <= gridMaxX; x += gridStep) {
ctx.beginPath();
ctx.moveTo(x, gridMinY);
ctx.lineTo(x, gridMaxY);
ctx.stroke();
}
for (let y = gridMinY; y <= gridMaxY; y += gridStep) {
ctx.beginPath();
ctx.moveTo(gridMinX, y);
ctx.lineTo(gridMaxX, y);
ctx.stroke();
}
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2 / scale;
ctx.beginPath();
ctx.moveTo(gridMinX, 0);
ctx.lineTo(gridMaxX, 0);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, gridMinY);
ctx.lineTo(0, gridMaxY);
ctx.stroke();
ctx.save();
ctx.scale(1, -1);
ctx.font = (12 / scale) + 'px Gadugi, Arial';
ctx.fillStyle = '#666666';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let x = gridMinX; x <= gridMaxX; x += gridStep) {
if (Math.abs(x) > 0.001) {
ctx.fillText(x.toFixed(0), x – 5 / scale, 10 / scale);
}
}
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘top’;
for (let y = gridMinY; y <= gridMaxY; y += gridStep) {
if (Math.abs(y) > 0.001) {
ctx.fillText(y.toFixed(0), 10 / scale, (y * -1) + 5 / scale);
}
}
ctx.restore();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = ‘#808080’;
ctx.lineWidth = 1 / scale;
ctx.beginPath();
ctx.arc(0, 0, d12_barycentric, 0, 2 * Math.PI);
ctx.stroke();
if (m3 > 0) {
ctx.strokeStyle = ‘#228b22’;
ctx.beginPath();
ctx.arc(posM2.x, posM2.y, d23, 0, 2 * Math.PI);
ctx.stroke();
}
if (m4 > 0) {
ctx.strokeStyle = ‘#8b008b’;
ctx.beginPath();
ctx.arc(0, 0, d14, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.setLineDash([]);
if (spacecraftActive) {
ctx.strokeStyle = ‘#ffa500’;
ctx.lineWidth = 2 / scale;
ctx.setLineDash([3, 3]);
ctx.beginPath();
const steps = 50;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const pos = computeTrajectory(t);
if (i === 0) {
ctx.moveTo(pos.x, pos.y);
} else {
ctx.lineTo(pos.x, pos.y);
}
}
ctx.stroke();
ctx.setLineDash([]);
}
if (lucyActive) {
ctx.strokeStyle = '#9333ea';
ctx.lineWidth = 2 / scale;
ctx.setLineDash([3, 3]);
ctx.beginPath();
for (let i = 0; i <= 30; i++) {
const pos = computeLucyTrajectory(lucyPhase, i / 30);
i === 0 ? ctx.moveTo(pos.x, pos.y) : ctx.lineTo(pos.x, pos.y);
}
ctx.stroke();
ctx.setLineDash([]);
if (lucyTrail.length > 0) {
ctx.strokeStyle = ‘#9333ea’;
ctx.lineWidth = 1 / scale;
ctx.globalAlpha = 0.3;
ctx.beginPath();
ctx.moveTo(lucyTrail[0].x, lucyTrail[0].y);
for (let i = 1; i < lucyTrail.length; i++) {
ctx.lineTo(lucyTrail[i].x, lucyTrail[i].y);
}
ctx.stroke();
ctx.globalAlpha = 1;
}
}
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1 / scale;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(barycenterM2M3.x, barycenterM2M3.y);
ctx.stroke();
if (m3 > 0) {
ctx.beginPath();
ctx.moveTo(posM2.x, posM2.y);
ctx.lineTo(posM3.x, posM3.y);
ctx.stroke();
}
if (m4 > 0) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(posM4.x, posM4.y);
ctx.stroke();
}
ctx.strokeStyle = ‘#ff0000’;
ctx.lineWidth = 3 / scale;
const lagKeys = [‘l2’, ‘l3’, ‘l4’, ‘l5’];
for (let i = 0; i < lagKeys.length; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(lPoints_m1m2[lagKeys[i]].x, lPoints_m1m2[lagKeys[i]].y);
ctx.stroke();
}
const lagKeys2 = ['l4', 'l5'];
for (let i = 0; i < lagKeys2.length; i++) {
ctx.beginPath();
ctx.moveTo(barycenterM2M3.x, barycenterM2M3.y);
ctx.lineTo(lPoints_m1m2[lagKeys2[i]].x, lPoints_m1m2[lagKeys2[i]].y);
ctx.stroke();
}
if (m3 > 0) {
ctx.strokeStyle = ‘#ffa500’;
ctx.lineWidth = 2 / scale;
const allLagKeys = [‘l1’, ‘l2’, ‘l3’, ‘l4’, ‘l5’];
for (let i = 0; i < allLagKeys.length; i++) {
ctx.beginPath();
ctx.moveTo(posM2.x, posM2.y);
ctx.lineTo(lPoints_m2m3[allLagKeys[i]].x, lPoints_m2m3[allLagKeys[i]].y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(posM3.x, posM3.y);
ctx.lineTo(lPoints_m2m3[allLagKeys[i]].x, lPoints_m2m3[allLagKeys[i]].y);
ctx.stroke();
}
}
if (m4 > 0) {
ctx.strokeStyle = ‘#8b008b’;
ctx.lineWidth = 3 / scale;
for (let i = 0; i < lagKeys.length; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(lPoints_m1m4[lagKeys[i]].x, lPoints_m1m4[lagKeys[i]].y);
ctx.stroke();
}
for (let i = 0; i < lagKeys2.length; i++) {
ctx.beginPath();
ctx.moveTo(posM4.x, posM4.y);
ctx.lineTo(lPoints_m1m4[lagKeys2[i]].x, lPoints_m1m4[lagKeys2[i]].y);
ctx.stroke();
}
}
const sizeM1 = 10 * Math.pow(m1, 1/3);
const sizeM2 = 10 * Math.pow(m2, 1/3);
const sizeM3 = 10 * Math.pow(m3, 1/3);
const sizeM4 = 10 * Math.pow(m4, 1/3);
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(0, 0, sizeM1, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#0000ff';
ctx.beginPath();
ctx.arc(posM2.x, posM2.y, sizeM2, 0, 2 * Math.PI);
ctx.fill();
if (m3 > 0) {
ctx.fillStyle = ‘#228b22’;
ctx.beginPath();
ctx.arc(posM3.x, posM3.y, sizeM3, 0, 2 * Math.PI);
ctx.fill();
}
ctx.fillStyle = ‘#ff0000’;
ctx.beginPath();
ctx.arc(barycenterM2M3.x, barycenterM2M3.y, 3, 0, 2 * Math.PI);
ctx.fill();
if (m4 > 0) {
ctx.fillStyle = ‘#8b008b’;
ctx.beginPath();
ctx.arc(posM4.x, posM4.y, sizeM4, 0, 2 * Math.PI);
ctx.fill();
}
if (spacecraftActive) {
const destPoint = getDestinationPoint();
const angle = Math.atan2(destPoint.y – spacecraftPosition.y, destPoint.x – spacecraftPosition.x);
const coneSize = 15;
ctx.save();
ctx.translate(spacecraftPosition.x, spacecraftPosition.y);
ctx.rotate(angle);
ctx.scale(1, -1);
ctx.fillStyle = ‘#ffa500’;
ctx.beginPath();
ctx.moveTo(coneSize, 0);
ctx.lineTo(-coneSize * 0.5, -coneSize * 0.5);
ctx.lineTo(-coneSize * 0.5, coneSize * 0.5);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = ‘#ff8c00’;
ctx.lineWidth = 2 / scale;
ctx.stroke();
ctx.restore();
}
if (lucyActive) {
const dx = lucyPosition.x – lucyPrevPosition.x;
const dy = lucyPosition.y – lucyPrevPosition.y;
let angle = lastLucyAngle;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1e-6) {
angle = Math.atan2(dy, dx);
setLastLucyAngle(angle);
} else {
const posAhead = computeLucyTrajectory(lucyPhase, Math.min(1, lucyProgress + 0.01));
const dxA = posAhead.x – lucyPosition.x;
const dyA = posAhead.y – lucyPosition.y;
const distA = Math.sqrt(dxA * dxA + dyA * dyA);
if (distA > 1e-6) {
angle = Math.atan2(dyA, dxA);
setLastLucyAngle(angle);
}
}
const coneSize = 15;
ctx.save();
ctx.translate(lucyPosition.x, lucyPosition.y);
ctx.rotate(angle);
ctx.scale(1, -1);
ctx.fillStyle = ‘#9333ea’;
ctx.beginPath();
ctx.moveTo(coneSize, 0);
ctx.lineTo(-coneSize * 0.5, -coneSize * 0.5);
ctx.lineTo(-coneSize * 0.5, coneSize * 0.5);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = ‘#7c3aed’;
ctx.lineWidth = 2 / scale;
ctx.stroke();
ctx.restore();
}
const dotSize = 5;
ctx.fillStyle = ‘#ff0000’;
const allKeys = [‘l1’, ‘l2’, ‘l3’, ‘l4’, ‘l5’];
for (let i = 0; i < allKeys.length; i++) {
ctx.beginPath();
ctx.arc(lPoints_m1m2[allKeys[i]].x, lPoints_m1m2[allKeys[i]].y, dotSize / scale, 0, 2 * Math.PI);
ctx.fill();
}
if (m3 > 0) {
ctx.fillStyle = ‘#ffa500’;
for (let i = 0; i < allKeys.length; i++) {
ctx.beginPath();
ctx.arc(lPoints_m2m3[allKeys[i]].x, lPoints_m2m3[allKeys[i]].y, dotSize / scale, 0, 2 * Math.PI);
ctx.fill();
}
}
if (m4 > 0) {
ctx.fillStyle = ‘#8b008b’;
for (let i = 0; i < allKeys.length; i++) {
ctx.beginPath();
ctx.arc(lPoints_m1m4[allKeys[i]].x, lPoints_m1m4[allKeys[i]].y, dotSize / scale, 0, 2 * Math.PI);
ctx.fill();
}
}
ctx.save();
ctx.scale(1, -1);
ctx.font = (14 / scale) + 'px Gadugi, Arial';
ctx.fillStyle = '#000000';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('M1', 0, (sizeM1 + 12 / scale));
ctx.fillText('M2', posM2.x, (posM2.y * -1) + sizeM2 + 12 / scale);
if (m3 > 0) {
ctx.fillText(‘M3’, posM3.x, (posM3.y * -1) + sizeM3 + 12 / scale);
}
if (m4 > 0) {
ctx.fillText(‘M4’, posM4.x, (posM4.y * -1) + sizeM4 + 12 / scale);
}
ctx.font = (12 / scale) + ‘px Gadugi, Arial’;
for (let i = 0; i < allKeys.length; i++) {
const offset = (i === 3) ? 15 / scale : (i === 4) ? -15 / scale : 15 / scale;
ctx.fillText('L' + (i + 1), lPoints_m1m2[allKeys[i]].x, (lPoints_m1m2[allKeys[i]].y * -1) + offset);
}
if (m3 > 0) {
ctx.fillStyle = ‘#ffa500’;
for (let i = 0; i < allKeys.length; i++) {
const offset = (i === 3) ? 12 / scale : (i === 4) ? -12 / scale : 12 / scale;
ctx.fillText("L" + (i + 1) + "'", lPoints_m2m3[allKeys[i]].x, (lPoints_m2m3[allKeys[i]].y * -1) + offset);
}
}
if (m4 > 0) {
ctx.fillStyle = ‘#8b008b’;
for (let i = 0; i < allKeys.length; i++) {
const offset = (i === 3) ? 15 / scale : (i === 4) ? -15 / scale : 15 / scale;
ctx.fillText("L" + (i + 1) + "\"", lPoints_m1m4[allKeys[i]].x, (lPoints_m1m4[allKeys[i]].y * -1) + offset);
}
}
if (spacecraftActive) {
ctx.fillStyle = '#ffa500';
ctx.font = (12 / scale) + 'px Gadugi, Arial';
ctx.fillText('JWST', spacecraftPosition.x, (spacecraftPosition.y * -1) - 20 / scale);
}
if (lucyActive) {
ctx.fillStyle = '#9333ea';
ctx.fillText('Lucy', lucyPosition.x, (lucyPosition.y * -1) - 20 / scale);
}
ctx.restore();
ctx.restore();
}, [m1, m2, m3, m3angle, m4, m4angle, m2angle, zoom, pan, barycenterM2M3, d12_barycentric, angle12_barycentric, lPoints_m1m2, lPoints_m2m3, lPoints_m1m4, posM2, posM3, posM4, d23, d14, spacecraftActive, spacecraftPosition, trajectoryType, destination, d3jwst, lucyActive, lucyPhase, lucyProgress, lucyPosition, lucyTrail, lucyPrevPosition, lastLucyAngle]);
useEffect(() => {
const container = canvasContainerRef.current;
if (!container) return;
const wheelHandler = (e) => {
e.preventDefault();
e.stopPropagation();
};
container.addEventListener(‘wheel’, wheelHandler, { passive: false });
return () => {
container.removeEventListener(‘wheel’, wheelHandler);
};
}, []);
const handleMouseDown = (e) => {
setIsDragging(true);
setLastMousePos({ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY });
};
const handleMouseMove = (e) => {
if (!isDragging) return;
const currentPos = { x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY };
setPan(prev => ({
x: prev.x + (currentPos.x – lastMousePos.x) / zoom,
y: prev.y + (currentPos.y – lastMousePos.y) / zoom
}));
setLastMousePos(currentPos);
};
const handleWheel = (e) => {
if (canvasContainerRef.current && canvasContainerRef.current.contains(e.target)) {
e.preventDefault();
e.stopPropagation();
setZoom(z => Math.max(0.5, Math.min(3, z * (e.deltaY < 0 ? 1.1 : 0.9))));
}
};
const phaseNames = ['', 'Ariane Rocket Launch', 'Earth Orbit 1', 'Earth Orbit 2', 'Transfer to L4"', 'L4" Survey', 'Return to Earth', 'Earth Orbit 3', 'Transfer to L5"', 'L5" Survey'];
return (
{showOutputPanel && (
{m3 > 0 && (
)}
{m4 > 0 && (
)}
{m3 > 0 && (
)}
{m4 > 0 && (
)}
)}
{!showOutputPanel && (
)}
`}
>
Orbital Mechanics v18.10
Restricted 3-Body Problem Simulator
(Lagrange Points)
{activeTab === ‘system’ && (
)}
{activeTab === ‘mission’ && (
{spacecraftActive && (
)}
)}
{missionSubTab === ‘lucy’ && m4 > 0 && (
)}
{lucyActive && lucyProgress >= 1 && lucyPhase < 9 && !fullMissionMode && (
)}
{lucyActive && lucyPhase === 9 && lucyProgress >= 1 && (
)}
{!m4 && missionSubTab === ‘lucy’ && (
)}
setM1(parseFloat(e.target.value))} className=”w-full” />
100
1000
setM2(parseFloat(e.target.value))} className=”w-full” />
1
10
setM3(parseFloat(e.target.value))} className=”w-full” />
{m3 > 0 && (
0
1
setM3angle(parseFloat(e.target.value))} className=”w-full” />
)}
0 degrees
360 degrees
setM4(parseFloat(e.target.value))} className=”w-full” />
{m4 > 0 && (
0
100
setM4angle(parseFloat(e.target.value))} className=”w-full” />
)}
0 degrees
360 degrees
{m4 > 0 && (
)}
{missionSubTab === ‘jwst’ && (
{[‘L1’, ‘L2’, ‘L3’, ‘L4’, ‘L5’].map(lp => (
))}
Progress:
{(spacecraftProgress * 100).toFixed(0)}%
setLucySpeed(parseFloat(e.target.value))}
className=”w-full”
/>
{lucyActive && (
25%
100%
Current Phase: {lucyPhase}/9
{phaseNames[lucyPhase]}
{(lucyProgress * 100).toFixed(0)}%
Lucy Mission Phases
{phaseNames.slice(1, 10).map((name, idx) => (
{idx + 1}.
{name}
))}
Mission Complete!
)}
Enable Jupiter (M4 greater than 0) to access Lucy mission
)}
Visualization
Speed:
setRevolutionSpeed(parseFloat(e.target.value))}
className=”w-24″
title={`Revolution Speed: ${(revolutionSpeed * 100).toFixed(0)}%`}
/>
{(revolutionSpeed * 100).toFixed(0)}%
Zoom: {zoom.toFixed(1)}x
Output Data
Mass Positions
M1: ({posM1.x.toFixed(1)}, {posM1.y.toFixed(1)})
M2: ({posM2.x.toFixed(1)}, {posM2.y.toFixed(1)})
d12: {d12.toFixed(2)}
M3: ({posM3.x.toFixed(1)}, {posM3.y.toFixed(1)})
d23: {d23.toFixed(2)}
d13: {d13.toFixed(2)}
M4: ({posM4.x.toFixed(1)}, {posM4.y.toFixed(1)})
d14: {d14.toFixed(2)}
Spacecraft Locations
{(spacecraftActive || lucyActive) ? (
{spacecraftActive && (
)}
{lucyActive && (
)}
) : (
JWST: ({spacecraftPosition.x.toFixed(1)}, {spacecraftPosition.y.toFixed(1)})
{m3 > 0 && (
d3jwst: {d3jwst.toFixed(2)}
)}
Lucy: ({lucyPosition.x.toFixed(1)}, {lucyPosition.y.toFixed(1)})
d2lucy: {d2lucy.toFixed(2)}
{m4 > 0 && (
d4lucy: {d4lucy.toFixed(2)}
)}
Phase {lucyPhase}/9
No active spacecraft
)}
Lagrange Point Locations
L1-L5 (M1-M2M3)
{[‘l1’, ‘l2’, ‘l3’, ‘l4’, ‘l5’].map((key, i) => (
L{i + 1}: ({lPoints_m1m2[key].x.toFixed(1)}, {lPoints_m1m2[key].y.toFixed(1)})
))}
L1-L5′ (M2-M3)
{[‘l1’, ‘l2’, ‘l3’, ‘l4’, ‘l5’].map((key, i) => (
L{i + 1}’ (M2-M3): ({lPoints_m2m3[key].x.toFixed(1)}, {lPoints_m2m3[key].y.toFixed(1)})
))}
L1-L5″ (M1-M4)
{[‘l1’, ‘l2’, ‘l3’, ‘l4’, ‘l5’].map((key, i) => (
L{i + 1}” (M1-M4): ({lPoints_m1m4[key].x.toFixed(1)}, {lPoints_m1m4[key].y.toFixed(1)})
))}
