公転する惑星は、太陽へ落ち続けながら横へ進んでいる

地球は1秒に約30 km進むあいだに、太陽の方へ約3 mm落ちている。落ちた分だけ進む向きが曲がり続け、1年かけて太陽を一周する。

記事

公転する惑星は、太陽へ落ち続けながら横へ進んでいる

地球は1秒に約30 km進むあいだに、太陽の方へ約3 mm落ちている。落ちた分だけ進む向きが曲がり続け、1年かけて太陽を一周する。

作成日 2026年11月3日・更新日 2026年11月3日・執筆・監修 佐々木 航

現象

月は1秒に1.4 mmずつ、地球へ落ちている

月は、地球から平均38万4,400 km離れた所を、1秒に約1 kmの速さで回っている1。地球の重力が急に消えたら、月はそのときの向きのまま、まっすぐ飛んでいくはずだ。ところが実際の月の道は、まっすぐな線から毎秒少しずつ地球の側へずれていく。そのずれは、1秒あたり約1.4 mm2。月は、地球に向かって落ち続けている。

それでも月が地上に届かないのは、横向きの速さがあるからだ。まっすぐ1 km進むだけでも、地球の中心からの距離はわずかに広がる。その広がりが、落ちる1.4 mmとちょうど同じになる速さで、月は横へ進んでいる。落ちる分と遠ざかる分が打ち消し合い、月は地球との距離をほぼ保ったまま回り続ける。

地球と太陽のあいだでも、同じことが起きている。地球は秒速約30 km(平均29.78 km)で太陽のまわりを回り、1秒に進む30 kmのあいだに、太陽の方へ約3 mm落ちる2。しかも速さは一定ではない。太陽に最も近い所では秒速30.29 km、最も遠い所では29.29 kmと、1 km/秒ほど変わる1。

画面の惑星も、同じ理屈で動く。シミュレーションは天体どうしが引き合う力を計算し、少しずつ時間を進めるだけだが、楕円の軌道も、太陽に近いほど速くなる動きも、ひとりでに現れる。何もない所からドラッグすれば、自分で天体を投げられる。

仕組み

横へ速く投げた石は、落ちても地面に届かない

ニュートンは、この「落ちながら回る」考えを1枚の図で示した。高い山の頂上から、石を水平に投げる。遅く投げた石はすぐ近くに落ち、速く投げるほど遠くに落ちる。地球は丸いので、さらに速く投げると、石が落ちていく先の地面も丸みに沿って下がっていく。そしてある速さに届くと、石は地面に触れないまま地球を一周して、投げた山に戻ってくる3。

1 2 3 高い山の頂から、水平に投げる 地表すれすれなら、秒速約7.9 kmで地球を一周する(円) もっと速いと、山から遠ざかる楕円になる 秒速約11.2 kmを超えると、戻ってこない 1 2 3 遅いほど近くに、速いほど遠くに落ちる 山の高さは大きく描いてある
山の頂上から水平に投げた物の道。速いほど遠くに落ち、ある速さで地球を一周し、もっと速いと戻ってこない

石を引き下ろすのは、地球の重力だ。ニュートンは、質量を持つ物はどれも互いに引き合い、その力は2つの質量の積に比例し、距離の2乗に反比例すると考えた。万有引力の法則である。

は引き合う力(N)、とは2つの物の質量(kg)、は2つの中心の距離(m)、は万有引力定数で N·m²/kg²だ4。距離が2倍になると、力は4分の1になる。

月の落ち方は、この「2乗に反比例」を数字で見せてくれる。月までの距離は、地球の半径(6,371 km)の約60倍。地上では落ちる物が1秒ごとに9.8 m/sずつ速くなるが、月が地球へ落ちる加速度はその約3,600分の1、つまり60の2乗分の1しかない2。地上で物を落とす力と、月を引き留めている力は、同じ重力が距離で弱まったものなのだ。

では、どれだけ速ければ落ちきらずに回れるのか。速さ(m/s)で半径の円を回る物は、向きを変え続けるために、円の中心へ向かうの加速度を必要とする。重力がちょうどこの分を受け持つと、円の軌道になる。

回る物の質量は、式から消えている。つまり、円を回る速さは中心の天体の質量と距離だけで決まり、小石でも惑星でも変わらない。太陽のと地球までの距離を入れると秒速約29.8 kmで、地球の実際の速さと合う。地表すれすれを回るなら、地球のと半径から秒速約7.9 kmになる5。

仕組み

楕円・面積・周期の3つは、同じ式から出てくる

円の速さより速い天体は太陽から離れていき、離れるにつれて引き戻されて遅くなり、やがて戻ってくる。遅い天体は太陽に近づき、近づくほど速くなって、また元の距離まで戻る。重力が距離の2乗に反比例するときは、この行き来の道がぴたりと閉じ、太陽を1つの焦点とする楕円になる。これがケプラーの第1法則だ。楕円のいちばん長い差し渡しの半分を、長半径と呼ぶ。

長半径a 2つの焦点までの距離の和は、どこでも2a 太陽(焦点) もう1つの焦点 近日点 速い 遠日点 遅い 斜線と点の部分は、同じ時間(周期の9分の1)に掃く面積。広さは等しい
楕円の軌道。太陽は焦点の1つにあり、同じ時間に掃く面積は太陽の近くでも遠くでも等しい

楕円の大きさは、ある場所での速さで決まる。1 kgあたりで数えた運動エネルギーと、重力による位置エネルギーの和は、軌道の上のどこでも変わらず、長半径だけで決まるからだ。

このため、同じ場所から速く投げるほどは大きくなる。速さが円の速さの倍(約1.41倍)に届くと左辺は0になり、は限りなく大きくなる。そこを超えた天体は、もう戻ってこない。地球の軌道の位置なら、秒速約42 kmが境目だ。

第2法則は、太陽と惑星を結ぶ線が同じ時間に同じ面積を掃く、というもの。重力はいつも太陽の向きに引くだけで、惑星を横へ押すことも止めることもない。そのため太陽のまわりを回る勢いは変わらず、短い時間に掃く細い三角形の面積が一定になる。

は、速さのうち太陽と結ぶ線に直角な向きの分である。距離が小さい所ではが大きくなり、遠い所では小さくなる。冒頭で見た地球の速さの変わり方も、このためだ。

第3法則は、公転の周期の2乗が長半径の3乗に比例する、というものである。円の軌道なら、一周の長さを上の速さで割れば周期になり、2乗すると次の式になる。楕円でも、を長半径に置き換えれば同じ形で成り立つ。

時間を年、長さを天文単位(AU)で測ると、太陽のまわりでは係数がちょうど1になり、と書ける。1 AUは太陽と地球の平均の距離で、約1億5,000万 kmだ。木星は太陽から約5.2 AU。5.2の3乗は約141で、公転周期11.86年の2乗も約141になる6。

試す

投げて、回して、見比べる

選んだ天体の、太陽から見た速さの移り変わりが、描画面の右上に線で出る。操作パネルの下には、距離・速さ・長半径・離心率(楕円のつぶれ具合で、0なら円)と、選んだ天体が実際に1周するのにかかった時間(測った周期)が並ぶ。天体をたたくと、その天体を選べる。

  1. 太陽系の場面は、地球を選んだ状態で始まる。距離は0.98〜1.02 AU、速さは秒速29.3〜30.3 kmのあいだを行き来し、地球が1周すると、測った周期1.00年と「T² ÷ a³」の1.000が出る。見る範囲を6 AUにして木星をたたき、ひと回りするのを待つと、測った周期は11.86年、長半径は5.20 AUで、「T² ÷ a³」はやはり1.000になる。周期の2乗と長半径の3乗がそろう第3法則のとおりに回っていることが分かる。
  2. 面積速度一定の場面では、惑星が太陽から0.43 AUまで近づき、2.08 AUまで遠ざかる細長い楕円を回る。周期の12分の1ごとに、太陽と結ぶ線が掃いた範囲が斜線と点で交互に塗られ、近くでは太く短い扇、遠くでは細長い扇になるが、12個そろっても、測った値の欄の「ばらつき」は1%ほどにとどまる。速さは太陽のそばで秒速約59 km、いちばん遠い所で約12 kmと、距離の比と同じく5倍近く違う。
  3. 太陽系の場面で、地球の軌道に近い何もない所から、太陽と結ぶ線に直角に、惑星と同じ反時計回りの向きへドラッグする。長さを左下の物差し(1 AU)と同じにすると秒速約30 kmになり、指を離す前に出る点線の予測はほぼ円になる。半分の長さ(秒速約15 km)なら水星の軌道より内側まで落ち込む細長い楕円になる。物差しの1.5倍(秒速約45 km)まで伸ばすと、測った値の欄に「閉じていない」と出て、投げた天体は戻ってこない。境目は円の速さの倍、秒速約42 kmである。
  4. スイングバイの場面では、探査機が長半径5.4 AUの楕円の上から、木星の通り道へ近づいていく。木星の約500万 km後ろを通り抜けると、右上の速さの線が跳ね上がり、太陽から見た速さは秒速約14 kmから16〜17 kmに上がって、長半径は約20 AUに延びる。木星から見れば、探査機は近づいたときと同じ速さで離れていくだけだが、向きが木星の進む向きへ曲げられた分、秒速13 kmで動く木星の勢いが上乗せされる。
  5. ラグランジュ点の場面は木星と一緒に回る向きから見た画面で、木星は右に止まって見える。木星の60°前(L4)と60°後ろ(L5)に置いた小惑星は、木星が3周する36年たってもどれも群れを離れず、60°のまわりを前後にゆっくり揺れ続ける。1往復には百数十年かかり、その道すじはおたまじゃくしのような形になる。太陽からの距離が木星の軌道から少しずれて置かれたものほど揺れが大きいのは、周期が木星とずれるからだ。一方、木星の真反対(L3)に置いた10個はとどまらず、軌道に沿って少しずつ流れていく。
  6. 連星の場面では、質量が太陽の半分の恒星2つが、1 AU離れて1年で回り合う。外側の惑星は2つの恒星の重心のまわりを約6.3年で回り、何十年たっても軌道はほとんど変わらない。恒星Aのそばの惑星は、もう1つの恒星Bに引かれて軌道が乱れ、2年もたたないうちに恒星Bにぶつかって1つになる。最初の位置がわずかに違うだけで、ぶつかる相手も時刻も変わってしまう。
現実では

ケプラーの計算から、ボイジャーとルーシーの道へ

ケプラーの法則のもとになったのは、デンマークの天文学者ティコ・ブラーエが望遠鏡を使わずに積み上げた惑星の観測である。ブラーエの助手になったケプラーは、火星の観測を円の軌道で説明しようとして何年も行き詰まり、やがて軌道が楕円だと気づいた。詳しい記録のあった惑星のうち、火星の軌道がいちばん細長かったことが、この発見を助けた。ケプラーは1609年の『新天文学』で第1と第2の法則を、1619年の『世界の調和』で第3法則を発表している7。

ただ、ケプラーは惑星を動かす力の正体を知らなかった。それを万有引力で説明したのが、1687年に出たニュートンの『プリンキピア』だ7。山の上から投げる図は、ニュートンの『世界の体系』にある。そのページの写真は、ボイジャー1号と2号が積んだ金色のレコードにも収められている3。

1977年8月20日に打ち上げられたボイジャー2号は、1979年に木星、1981年に土星、1986年に天王星、1989年に海王星のそばを通った。木星・土星・天王星・海王星をこの順にたどれる並びは、約175年に1度しか来ない。探査機は惑星のそばを通るたびに道を曲げられ、次の惑星へ届く速さを得た。だから、海王星までの時間は30年から12年に縮んだ8。NASAジェット推進研究所(JPL)の解説によると、木星がそこにいなければ、ボイジャー2号は太陽の方へ落ちて戻り、楕円の軌道を回り続けたはずだった。探査機を速めた分、木星は公転のエネルギーを少し失っている9。

木星の60°前と後ろには、本物の小惑星の群れがある。太陽・木星と正三角形をつくるL4・L5のまわりにとどまる木星トロヤ群で、群れから木星までの距離は、木星から太陽までと同じ約7.8億 kmだ。NASAは2026年10月28日、2021年10月16日に打ち上げた探査機ルーシーが、前の群れ(L4)の領域に入った初めての探査機になったと伝えた。最初に訪ねるのはエウリュバテスで、2027年8月11日の予定だ。2030年には地球のそばへ戻って3回目で最後のスイングバイをし、後ろの群れ(L5)へ向かう。2033年3月に、パトロクロスとメノイティオスの組のそばを通る10。

省いていること

本物の宇宙と違うところ

  • 天体はすべて1枚の平面の上を動く。本物の惑星の軌道面は少しずつ傾いていて、水星は地球の軌道面から7.0°傾いている1
  • 重力はニュートンの式だけで計算する。探査機の精密な航法で使うような、相対性理論による補正は入れていない11
  • 重力は天体を点とみなして計算し、約1万5,000 kmより近い所では弱めている。ぶつかったかどうかは、時間を進める細かい刻みごとに、2つの中心の距離で決める。基準は、それぞれの半径(太陽は約70万 km、惑星は質量から見積もった値)に画面に描いた丸の半径の4分の1を足した長さの和で、小惑星と探査機には画面の丸の分を足さない。ぶつかった2つは運動量を保ったまま1つにまとめ、かけらや熱は出ない
  • 太陽の1億分の1より軽い天体(小惑星や探査機)は、ほかの天体を引かない。太陽から400 AUより遠くへ出た天体は、計算から外す
  • 惑星の並びは、ある日付の本物の並びではない。どの惑星も近日点から動き始め、衛星はいない。スイングバイとラグランジュ点の場面の木星は円を回る(本物の木星の離心率は0.049)1
参考文献

参考文献

  1. NASA Space Science Data Coordinated Archive「Planetary Fact Sheets」(Earth・Moon・Sun・Planetary Fact Sheet - Metric)2024〜2025. https://nssdc.gsfc.nasa.gov/planetary/planetfact.html
  2. National Institute of Standards and Technology「CODATA Value: Newtonian constant of gravitation」2022年の推奨値, 2024. https://physics.nist.gov/cgi-bin/cuu/Value?bg
  3. NASA Science Editorial Team「Orbits and Kepler's Laws」NASA Science, 2024. https://science.nasa.gov/solar-system/orbits-and-keplers-laws/
  4. Newton, I.『Philosophiae Naturalis Principia Mathematica』London, 1687. 英訳はMotte, A.訳『The Mathematical Principles of Natural Philosophy』1729. https://en.wikisource.org/wiki/The_Mathematical_Principles_of_Natural_Philosophy_(1729)
  5. NASA Science「Page of book (Newton, System of the World)」Voyager Golden Recordの画像. https://science.nasa.gov/image-detail/page-of-book-newton-system-of-the-world-31297500982-o/
  6. NASA Science「Voyager 2」「Planetary Voyage」. https://science.nasa.gov/mission/voyager/voyager-2/ https://science.nasa.gov/mission/voyager/planetary-voyage/
  7. NASA Science「Basics of Space Flight, Chapter 4: Trajectories」. https://science.nasa.gov/learn/basics-of-space-flight/chapter4-1/
  8. NASA Science「Lucy Mission: Frequently Asked Questions」とKretke, K.「NASA's Lucy Reaches Trojan Asteroid Region 5 Years After Launch」NASA Science, 2026. https://science.nasa.gov/mission/lucy/frequently-asked-questions/ https://science.nasa.gov/blogs/lucy/2026/10/28/nasas-lucy-reaches-trojan-asteroid-region-5-years-after-launch/

注

  1. 参考文献1。月の平均距離384,400 km・平均の速さ1.022 km/秒、地球の平均の速さ29.78 km/秒(最大30.29・最小29.29)、水星の軌道傾斜7.0°、木星の離心率0.049。 ↩ ↩2 ↩3 ↩4

  2. 参考文献1の値から計算した。円を回る物が中心へ向かう加速度に、月なら1.022 km/秒と384,400 km、地球なら29.78 km/秒と1億4,960万 kmを入れ、1秒間に落ちる距離(加速度 × 1秒の2乗 ÷ 2)を求めた。地上の値には、地球の平均の重力加速度9.82 m/s²を使った。 ↩ ↩2 ↩3

  3. 参考文献5。NASAが公開しているレコードの画像には、地球の表面と、高い山の頂から水平に投げた物が描く曲線の図が写っている。 ↩ ↩2

  4. 参考文献2。推奨値は m³/(kg·s²)で、N·m²/kg²と同じ単位である。 ↩

  5. 参考文献1。太陽のは m³/s²、地球のは m³/s²、地球の赤道半径は6,378 km。地球を振り切る速さは秒速11.19 km。 ↩

  6. 参考文献1。木星の太陽からの距離は7億7,850万 km(5.20 AU)、公転周期は4,331日(11.86年)。 ↩

  7. 参考文献3・4。 ↩ ↩2

  8. 参考文献6。 ↩

  9. 参考文献7。 ↩

  10. 参考文献8。木星から群れまでの距離は、NASAの発表では約4億8,400万マイル。 ↩

  11. 参考文献3。 ↩

プログラム
プログラム

このシミュレーションのプログラム

このシミュレーションは、1コマごとに「すべての天体の組について重力を計算する → ほんの少しだけ時間を進める」をくり返している。時間の刻みは、天体どうしが近づくほど細かくなる。画面の楕円や面積は、その結果から計算して描いている。

長さはAU、時間は年

距離の単位は、太陽と地球の平均の距離を1とする天文単位(AU)、時間の単位は年、質量は太陽を1とする。この単位では万有引力定数がになり、地球はちょうど1年で一回りする。画面に出す速さは、1 AU/年 = 4.74 km/秒で換算している。

定数と単位(src/sims/orbits.ts の 5〜9 行)
const G = 4 * Math.PI * Math.PI;const KMS = 4.74047; // 1 AU/年 = 4.74 km/秒const EPS2 = 1e-8; // ぶつかる直前の計算が発散しないように(1e-4 AU)const SRC = 1e-8; // これより軽い天体は、ほかの天体を引かない(小惑星・探査機)const TRAIL = 360;

EPS2は、2つの天体が重なるほど近づいたとき、距離で割る計算が無限大にならないようにする小さな値である。

すべての組の重力を足す

天体iが天体jから受ける加速度の大きさはで、向きはjのほうを向く。コードは天体の組を1回ずつ調べ、iがjを引く力と、jがiを引く力(作用と反作用)を同時に足している。小惑星や探査機のように軽い天体は、ほかの天体をほとんど引かないので、引く側の計算を省いている(SRCより軽い天体)。

重力の計算(src/sims/orbits.ts の 44〜56 行)
function accel(B: Body[]) {  for (const b of B) { b.ax = 0; b.ay = 0; }  for (let i = 0; i < B.length; i++) {    const a = B[i];    for (let j = i + 1; j < B.length; j++) {      const b = B[j];      if (a.m < SRC && b.m < SRC) continue;      const dx = b.x - a.x, dy = b.y - a.y, r2 = dx * dx + dy * dy + EPS2, inv = G / (r2 * Math.sqrt(r2));      if (b.m >= SRC) { a.ax += dx * inv * b.m; a.ay += dy * inv * b.m; }      if (a.m >= SRC) { b.ax -= dx * inv * a.m; b.ay -= dy * inv * a.m; }    }  }}

inv = G / (r2 * Math.sqrt(r2))はにあたる。向きを表す(dx, dy)は長さを含むので、これを掛けると、大きさがでjを向いた加速度になる。

時間を少しずつ進める

速さと位置を、短い時間ずつ進める。使っているのはリープフロッグ法(かえる跳び)で、速さを半分だけ進める → 位置を進める → 新しい位置で重力を計算する → 速さを残りの半分進める、の順に計算する。単純な方法だが、何百周しても全体のエネルギーがずれにくく、軌道が渦巻きのように広がったり縮んだりしない。

時間を進める(src/sims/orbits.ts の 57〜70 行)
/** 刻みの長さ: いちばん速く向きを変える天体に合わせる(近づくほど細かく) */function stepSize(B: Body[], hmax: number) {  let h = hmax;  for (const b of B) {    const a = Math.hypot(b.ax, b.ay);    if (a > 0) { const v = Math.hypot(b.vx, b.vy) + 1e-9; h = Math.min(h, 0.012 * v / a, 0.05 * Math.sqrt(1 / a)); }  }  return Math.max(h, 2e-6);}function leap(B: Body[], h: number) {  for (const b of B) { b.vx += b.ax * h * 0.5; b.vy += b.ay * h * 0.5; b.x += b.vx * h; b.y += b.vy * h; }  accel(B);  for (const b of B) { b.vx += b.ax * h * 0.5; b.vy += b.ay * h * 0.5; }}

刻みは、加速度が大きいほど(太陽に近いほど)小さくする。水星のそばや、天体どうしがすれ違う場面で、計算が一気に飛ばないようにするためである。

速さと位置から楕円を求める

画面の点線の楕円は、天体の今の位置と速さだけから求めた「このまま中心の天体の重力だけで動いたら描く楕円」である。天文学では接触軌道と呼ぶ。

軌道の形(src/sims/orbits.ts の 71〜91 行)
type Prim = { x: number; y: number; vx: number; vy: number; m: number; name: string };/** 中心の天体: いちばん重いほか。2 つの恒星(重さが近い)の外側を回るときは、2 つの重心を中心にする */const primaryOf = (B: Body[], me: Body): Prim | null => {  const o = B.filter((b) => b !== me).sort((a, b) => b.m - a.m);  if (!o.length) return null;  const [a, b] = o;  if (b && b.m > 0.1 * a.m) {    const d = Math.hypot(a.x - b.x, a.y - b.y), M = a.m + b.m;    const cx = (a.x * a.m + b.x * b.m) / M, cy = (a.y * a.m + b.y * b.m) / M;    if (Math.hypot(me.x - cx, me.y - cy) > d * 1.6) return { x: cx, y: cy, vx: (a.vx * a.m + b.vx * b.m) / M, vy: (a.vy * a.m + b.vy * b.m) / M, m: M, name: `${a.name}と${b.name}の重心` };  }  return a;};/** 軌道の形(中心の天体に対する接触軌道要素) */function elements(me: Body, P: Prim) {  const mu = G * (P.m + me.m), rx = me.x - P.x, ry = me.y - P.y, vx = me.vx - P.vx, vy = me.vy - P.vy;  const r = Math.hypot(rx, ry), v2 = vx * vx + vy * vy, en = v2 / 2 - mu / r;  const rv = rx * vx + ry * vy, ex = ((v2 - mu / r) * rx - rv * vx) / mu, ey = ((v2 - mu / r) * ry - rv * vy) / mu;  const e = Math.hypot(ex, ey), a = -mu / (2 * en);  return { r, v: Math.sqrt(v2), e, ex, ey, a, T: en < 0 ? TAU * Math.sqrt(a * a * a / mu) : Infinity, bound: en < 0 };}

まず1 kgあたりのエネルギーを求める()。長半径はで、が負なら楕円、0以上なら戻ってこない軌道になる。離心率は、中心から近日点の向きを指す「離心率ベクトル」の長さで求める。周期はケプラーの第3法則から出す。画面の「測った周期」は、これとは別に、天体が中心のまわりを実際に1周するのにかかった時間を数えたものである(1コマの計算の章)。その周期と長半径から出す「T² ÷ a³」が1に近いことが、第3法則を数字で示している(中心と天体の質量の和を掛け、法則どおりなら1になる形で出している)。

2つの恒星の外側を回る惑星では、2つの恒星の重心を中心とみなす。片方の恒星だけを中心にすると、長半径が100 AU近い、ありえない楕円になってしまう。

投げる前に軌道を描く

ドラッグしている間、指を離したらどう動くかを、天体の写しを使って先に計算し、点線で描いている。

予測の線(src/sims/orbits.ts の 157〜173 行)
function predict(s: S, x: number, y: number, vx: number, vy: number, api: Api) {  const B = s.bodies.filter((b) => b.m >= SRC).map((b) => ({ ...b }));  const t = body("予測", x, y, vx, vy, 1e-12, 0, "small");  const all = [...B, t]; accel(all);  const Z = ZOOM[api.str("zoom")] ?? ZOOM["2"];  const out: number[] = [x, y];  let time = 0, lx = x, ly = y; const tmax = Z.rate * 14;  for (let k = 0; k < 2400 && time < tmax; k++) {    const h = stepSize(all, Z.R * 0.004);    leap(all, h); time += h;    if (Math.hypot(t.x - lx, t.y - ly) > Z.R * 0.012) { out.push(t.x, t.y); lx = t.x; ly = t.y; }    if (Math.hypot(t.x, t.y) > Z.R * 3) break;    let hit = false; for (const b of B) if (Math.hypot(b.x - t.x, b.y - t.y) < Z.R * 0.01) hit = true;    if (hit) break;  }  return out;}
投げる操作(src/sims/orbits.ts の 232〜261 行)
pointer(type, api, s) {  const v = s.view; if (!v) return;  const th = s.theta, P = api.pointer;  const toWorld = (px: number, py: number) => { const [x, y] = [v.ix(px), v.iy(py)]; const c = Math.cos(th), sn = Math.sin(th); return [x * c - y * sn, x * sn + y * c]; };  if (type === "down") {    // 天体の上なら選ぶ    let best: Body | null = null, bd = api.u(16);    for (const b of s.bodies) { const [rx, ry] = rot(b.x, b.y, th); const d = Math.hypot(v.X(rx) - P.x, v.Y(ry) - P.y); if (d < bd) { bd = d; best = b; } }    if (best) { s.sel = best; s.sectors = []; s.secPts = []; s.drag = null; return; }    const [x, y] = toWorld(P.x, P.y);    s.drag = { x, y, cx: x, cy: y, on: true, moved: false, path: [], dirty: true };  } else if (type === "move" && s.drag?.on) {    const [x, y] = toWorld(P.x, P.y);    s.drag.cx = x; s.drag.cy = y; s.drag.dirty = true;    if (Math.hypot(P.x - P.x0, P.y - P.y0) > 6) s.drag.moved = true;  } else if ((type === "up" || type === "cancel") && s.drag) {    const d = s.drag; s.drag = null;    if (type === "cancel" || !d.moved) return;    const Z = ZOOM[api.str("zoom")] ?? ZOOM["2"];    const k = (4 * Math.PI) / Z.R;    // 回して見ているときも、ドラッグの向きと長さをそのまま太陽系に対する速さにする(座標の回転の分は足さない)    let vx = (d.cx - d.x) * k, vy = (d.cy - d.y) * k;    const M = MASS[api.str("mass")] ?? MASS.earth;    s.thrown++;    const nb = body(`投げた天体 ${s.thrown}`, d.x, d.y, vx, vy, M.m, M.px, M.kind);    // 投げる前に重い天体の動きを持ち込まない(太陽系の中心から見た速さ)    s.bodies.push(nb); accel(s.bodies); s.sel = nb; s.sectors = []; s.secPts = [];    api.emit("throw", { mass: api.str("mass") });  }},

ドラッグの長さを速さに直す割合は(は見る範囲の半径)にしてある。見る範囲が半径2 AUなら、左下の物差し1本分(1 AU)のドラッグが秒速約30 kmになり、太陽から1 AUの所で円を描く速さと同じになる。この割合なら、太陽に落ちる軌道から飛び去る軌道まで、指の動きだけで作れる。

1コマの計算と、掃く面積

1コマの間に進める時間は、「時間の速さ」のつまみ × 見る範囲ごとの倍率で決まる。その時間を、上の刻みで何回にも分けて進める(1コマ2,500回まで)。刻みを1つ進めるたびに、ぶつかった天体がないかをcollideで調べる。1コマの終わりにまとめて調べると、速い天体が太陽をすり抜けてしまうからである。

1コマの計算(src/sims/orbits.ts の 265〜310 行)
// ---- 進めるif (api.dt > 0) {  let T = api.dt * Z.rate * api.num("rate"), steps = 0;  const kView = viewOf(api).k || 1;  const hmax = Z.R * 0.003;  while (T > 1e-12 && steps < 2500) {    const h = Math.min(stepSize(B, hmax), T);    leap(B, h); T -= h; s.time += h; steps++;    collide(B, s, api, kView);    // 軌跡: 画面の上で少し動くたびに点を足す(1 コマごとだと速い水星の軌跡が角張る)    const gap = Z.R * 0.006;    for (const b of B) { const i = (b.ti - 1 + TRAIL) % TRAIL; if (!b.tn || Math.hypot(b.x - b.tx[i], b.y - b.ty[i]) > gap) addTrail(b, s.theta); }    // 周期を測る: 中心から見た向きの変化を足し合わせ、1 周(2π)に届いた時間を測った周期にする    if (s.sel) {      const P0 = primaryOf(B, s.sel);      if (P0) {        const ang = Math.atan2(s.sel.y - P0.y, s.sel.x - P0.x), pr = s.per;        if (pr.for !== s.sel) { pr.for = s.sel; pr.ang = ang; pr.acc = 0; pr.t = 0; pr.T = 0; }        else {          let d = ang - pr.ang; if (d > Math.PI) d -= TAU; if (d < -Math.PI) d += TAU;          pr.acc += d; pr.ang = ang; pr.t += h;          if (Math.abs(pr.acc) >= TAU) { pr.T = pr.t; pr.acc -= Math.sign(pr.acc) * TAU; pr.t = 0; }        }      }    }    // 面積速度: 選んだ天体の位置を、短い間隔で集める    if (s.sel && api.bool("areas")) {      const P = primaryOf(B, s.sel);      if (P) {        const el = elements(s.sel, P);        if (el.bound && el.e < 0.97) {          if (s.secFor !== s.sel) { s.secFor = s.sel; s.sectors = []; s.secPts = []; s.secT = 0; }          s.secT += h;          const last = s.secPts.length;          const rx = s.sel.x - P.x, ry = s.sel.y - P.y;          if (!last || Math.hypot(rx - s.secPts[last - 2], ry - s.secPts[last - 1]) > el.a * 0.01) s.secPts.push(rx, ry);          if (s.secT >= el.T / 12) {            const pts = [0, 0, ...s.secPts, rx, ry];            s.sectors.push({ pts, area: shoelace([...pts, 0, 0]) });            if (s.sectors.length > 12) s.sectors.shift();            s.secPts = [rx, ry]; s.secT = 0;          }        }      }    }  }

「掃く面積」では、周期の12分の1の時間ごとに、太陽と天体を結ぶ線が通った範囲を多角形として集め、頂点の座標から面積を求める式(靴ひも式)で面積を計算している。12個の面積のばらつきが1%ほどに収まることが、ケプラーの第2法則を数字で示している。区切りは刻みの境目でしか切れないので、1秒のコマ数が少ない端末では刻みが粗くなり、ばらつきが少し大きく出る。

描く

描く(src/sims/orbits.ts の 326〜410 行)
// ---- 描くconst v = (s.view = viewOf(api));api.clear();for (let i = 0; i < s.stars.length; i += 3) api.dot(s.stars[i] * api.w, s.stars[i + 1] * api.h, s.stars[i + 2] < 0.85 ? api.u(0.5) : api.u(0.9));const P = (x: number, y: number): [number, number] => { const [rx, ry] = rot(x, y, th); return [v.X(rx), v.Y(ry)]; };// 掃く面積(交互の斜線)if (api.bool("areas") && s.sel && s.secFor === s.sel) {  const prim = primaryOf(B, s.sel);  if (prim) {    s.sectors.forEach((sc, i) => {      const pts: [number, number][] = [];      for (let k = 0; k + 1 < sc.pts.length; k += 2) pts.push(P(sc.pts[k] + prim.x, sc.pts[k + 1] + prim.y));      if (i % 2 === 0) api.hatch(pts, { gap: api.u(4.5), angle: 45, w: api.u(0.8) }); else api.dots(pts, { gap: api.u(5), r: api.u(0.8) });      api.poly(pts, { close: true, w: api.u(0.6) });    });  }}// 軌跡(古いほど細く)for (const b of B) {  if (b.tn < 2) continue;  const n = b.tn, start = (b.ti - n + TRAIL) % TRAIL;  const ctx = api.ctx, R = api.bool("rot"), seg = 6;  ctx.strokeStyle = api.ink; ctx.setLineDash([]);  for (let g = 0; g < seg; g++) {    const k0 = Math.floor((g * (n - 1)) / seg), k1 = Math.floor(((g + 1) * (n - 1)) / seg);    ctx.beginPath();    for (let k = k0; k <= k1; k++) {      const i = (start + k) % TRAIL;      const [rx, ry] = R ? rot(b.tx[i], b.ty[i], b.ta[i]) : [b.tx[i], b.ty[i]];      if (k === k0) ctx.moveTo(v.X(rx), v.Y(ry)); else ctx.lineTo(v.X(rx), v.Y(ry));    }    ctx.lineWidth = api.u(0.25 + 1.4 * ((g + 1) / seg) * (b.kind === "small" ? 0.5 : 1)); ctx.stroke();  }}// 楕円と焦点if (api.bool("ellipse") && s.sel && !api.bool("rot")) {  const prim = primaryOf(B, s.sel);  if (prim) {    const el = elements(s.sel, prim);    if (el.bound && el.e < 0.995 && el.a < ZOOM["40"].R * 4) {      const b2 = el.a * Math.sqrt(1 - el.e * el.e), ang = Math.atan2(el.ey, el.ex);      const cx = prim.x - el.a * el.ex, cy = prim.y - el.a * el.ey;      const pts: [number, number][] = [];      for (let k = 0; k <= 160; k++) { const t = (k / 160) * TAU, ex = el.a * Math.cos(t), ey = b2 * Math.sin(t); pts.push(P(cx + ex * Math.cos(ang) - ey * Math.sin(ang), cy + ex * Math.sin(ang) + ey * Math.cos(ang))); }      api.poly(pts, { w: api.u(1), dash: [api.u(4), api.u(4)] });      const [fx, fy] = P(prim.x - 2 * el.a * el.ex, prim.y - 2 * el.a * el.ey), c = api.u(5);      api.line(fx - c, fy, fx + c, fy, { w: api.u(1) }); api.line(fx, fy - c, fx, fy + c, { w: api.u(1) });      if (el.e > 0.08) api.label("もう 1 つの焦点", fx + api.u(8), fy - api.u(8), { size: api.u(11) });    }  }}// 天体for (const b of B) {  const [X, Y] = P(b.x, b.y), r = api.u(b.px);  if (b.kind === "sun" || b.kind === "star") {    api.circle(X, Y, r, { fill: "ink" });    const n = 14;    for (let k = 0; k < n; k++) { const a = (k / n) * TAU + s.spin * 0.15, r1 = r + api.u(3), r2 = r + api.u(k % 2 ? 5 : 9); api.line(X + Math.cos(a) * r1, Y + Math.sin(a) * r1, X + Math.cos(a) * r2, Y + Math.sin(a) * r2, { w: api.u(1) }); }  } else if (b.kind === "small") api.dot(X, Y, Math.max(api.u(1.2), r * 0.6));  else { api.circle(X, Y, r, { fill: "paper", w: api.u(1.4) }); api.circle(X, Y, r * 0.45, { fill: "ink", stroke: false }); }  if (b === s.sel) { api.circle(X, Y, r + api.u(7), { w: api.u(1), dash: [api.u(2), api.u(3)] }); api.label(b.name, X + r + api.u(10), Y - r - api.u(6), { size: api.u(12.5) }); }  else if (b.kind === "planet" && (api.str("zoom") !== "40" || b.m > 1e-5)) api.text(b.name, X + r + api.u(5), Y + api.u(4), { size: api.u(11) });}// 投げる前の矢印と予測の軌道if (s.drag?.on && s.drag.moved) {  const d = s.drag, k = (4 * Math.PI) / Z.R;  if (d.dirty) { d.path = predict(s, d.x, d.y, (d.cx - d.x) * k, (d.cy - d.y) * k, api); d.dirty = false; }  const pts: [number, number][] = []; for (let i = 0; i + 1 < d.path.length; i += 2) pts.push(P(d.path[i], d.path[i + 1]));  api.poly(pts, { w: api.u(1.2), dash: [api.u(1.5), api.u(4)] });  const [x0, y0] = P(d.x, d.y), [x1, y1] = P(d.cx, d.cy);  api.circle(x0, y0, api.u(4), { fill: "ink" });  api.arrow(x0, y0, x1, y1, { w: api.u(1.6) });  api.label(`${fmt(Math.hypot(d.cx - d.x, d.cy - d.y) * k * KMS, 1)} km/秒`, x1 + api.u(10), y1, { size: api.u(12), font: "mono" });}// 目盛り: 1 AU の物差しと経過時間const L = Z.R >= 40 ? 10 : 1, lx = api.u(18), ly = api.h - api.u(18), lw = L * v.k;api.line(lx, ly, lx + lw, ly, { w: api.u(1.2) }); api.line(lx, ly - api.u(4), lx, ly + api.u(4), { w: api.u(1.2) }); api.line(lx + lw, ly - api.u(4), lx + lw, ly + api.u(4), { w: api.u(1.2) });api.text(`${L} AU`, lx + lw + api.u(8), ly + api.u(4), { size: api.u(11.5), font: "mono" });api.label(`経過 ${fmt(s.time, 2)} 年`, api.u(16), api.u(26), { size: api.u(12.5), font: "mono" });if (s.sel && s.speedHist.length > 2) {  // 目盛りの幅は平均の 30% 以上(地球の 29〜30 km/秒のような小さな変化を大きく見せない)  let lo = Infinity, hi = -Infinity; for (const x of s.speedHist) { lo = Math.min(lo, x); hi = Math.max(hi, x); }  const mid = (lo + hi) / 2, span = Math.max(hi - lo, mid * 0.3);  api.spark(s.speedHist, api.w - api.u(170), api.u(56), api.u(150), api.u(36), { min: Math.max(0, mid - span / 2), max: mid + span / 2, label: "太陽から見た速さ(km/秒)" });}

色は白と黒の2色だけである。軌跡は古いほど細い線にして、時間の流れを線の太さで表している。面積の区切りは、斜線と点を交互に使って見分けられるようにしている。

画面の部品(描画・場面の切り替え・画質の自動調整・GPU の網点)は全記事で共通です。シミュレーションの共通の仕組みで解説しています。

プログラム全体を見る(src/sims/orbits.ts・434 行)

読み込んでいます

姉妹サイト

株式会社Game が運営するメディアです。