GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

銀河 シミュレーション 9種 — ぶつかり合う星が渦巻腕と潮汐尾を作る

ライブラリ・WebGLなしで、星ひとつひとつに毎フレームニュートン重力を積分すると、2つの銀河がすれ違い潮汐尾を残して合体します。宇宙規模の引力がそのまま目の前で起こるシーンをコピペで持ち帰れます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

デモ

九つの盤はそれぞれ index.html と style.css の2ファイルで自立し、開いた瞬間から動きます。24フレームの録画区間で九つの盤すべてが23フレーム動き続け、星の位置は毎フレーム最初から再計算されるので、画面のどの星も止まりません。07番はドラッグで星を投げ込め、04番は見えない質量のトグルを押せるので、通り過ぎてしまう前に一度触ってみてください。

項目 焦点 累積面積(%) 強度平均
01 すれ違いと尾 2つの円盤がすれ違い尾を吐く基本場面 20.125 59.3
02 腕の成長 差動回転が円盤を渦巻きへ巻き上げる過程 31.31 48.0
03 重心の周回 球状星団がそれぞれの軌道面で回る形 22.827 49.4
04 見えない質量 ハローのトグルで変わる外側の回転速度 37.479 50.5
05 二度目の接近以降 渦巻き腕がほどけて楕円体へ固まる結末 32.382 48.4
06 一つの星の軌跡 尾を作る個別軌道の往復 20.762 55.0
07 ドラッグ発射 角度と強さを手で決めて彗星を投入 26.731 55.3
08 積もる残像 消されない軌道が描く渦の文様 32.485 62.6
09 深宇宙の舞台 3つの銀河が順番にすれ違う締めの大画面 30.189 52.2

累積面積が最も広いのは04の37.479%で、最も狭いのは01の20.125%です。01・03・06・07は星の点そのものが画面の狭い面積しか占めない主題なので20.125〜26.731%の帯に留まりますが、これは場面の性質であって計算の量ではありません。フレーム変化の強度は08が62.6で最も明るく、04の右上のグラフに描かれる平らな回転曲線は、銀河の外側の星が予想より速く回るという観測そのものです。

衝突の前から後まで、時間軸に並べた九つの場面

並び順の基準は見栄えではなく、引力の相手がどこにいるかです。01は2つの円盤が互いの星を引き合うすれ違いの基本で、02は描かない中心質量の引力と星同士の重力を与えた静止円盤が自分で腕を育てる過程、03は円盤ではなく球に撒いた星が重心の周りをそれぞれの軌道面で回る対照例です。04は九つの中で唯一、星同士の直接計算を半径の格子に置き換える近似の盤で、05は一度すれ違った後の2回目から始めて合体の結末を見せ、06は340個の星の中の1個の軌跡だけを太く残します。07で初めて手で星を投げ、08で残像を消さずに積み、09が星雲の舞台で締めくくります。星自身が引力の源である点では、すれ違う個体の位置だけを見るボイドや、濃度の勾配だけを頼りに道を伸ばす粘菌の9盤と同種の面白さです。

銀河 衝突 シミュレーション が扱いにくい理由

銀河 衝突 シミュレーション が難しいのは、衝突が1回の出来事ではなく、何億年もかけてすれ違いと引きちぎりを繰り返す過程だからです。1枚の絵として描くこともできますが、この記事では星ごとの軌道積分から構造を再現するので、近づく速さとすれ違いの距離を決めるだけで尾の形が変わります。

項目別コード

項目ごとに、挙動を決める定数名を示し、動いている関数をそのまま引用します。切り詰めた断片ではなく動いているコードそのものなので、この画面から自分のページへ貼って動かせます。コメントだけこの記事の言葉に置き換えてあり、変数名と関数名はzipの中身と同じ英語のままです。

012つの銀河衝突 — すれ違い潮汐尾を残して合体する基本シーン

星340個を2つの円盤に播種し、円盤の縁が円軌道速度になるよう中心質量 M = 0.90·R² を与えて、反対側から接近させます。すれ違う瞬間に外側の星が2つの中心の間を行き来してエネルギーを失い、外へ放り出される潮汐尾がひとりでに育ち、全ペアを直接計算する近似なしの盤です。2.0秒のループでフレーム最大16.048%は盤の中でも大きいほうであり、1フレームごとの変化が激しいことを示しています。

2つの銀河衝突 — すれ違い潮汐尾を残して合体する基本シーン
新しいタブで開く

01 · 2つの銀河衝突 — すれ違い潮汐尾を残して合体する基本シーン — 計測 累積面積 20.125% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DarcrequestAnimationFrame

全ペア重力とsemi-implicit Eulerの最小構成

重力は a_i = Σ_j G·(x_j − x_i)/(|d|²+ε²)^1.5 で、ペアの対称性を使って1ペア1回の計算で両側に反映します。G=140、ε²=16、DT=1/60で、速度を更新してから位置を動かすsemi-implicit Eulerです。

  function step() {
    // 全ペア重力 — a_i = Σ_j G·(x_j - x_i)/(|d|²+ε²)^1.5。対称なのでペアごとに1回計算して両側に反映
    var AX = new Array(N), AY = new Array(N);
    for (var i = 0; i < N; i++) { AX[i] = 0; AY[i] = 0; }
    for (var i = 0; i < N; i++) {
      for (var j = i + 1; j < N; j++) {
        var dx = X[j] - X[i], dy = Y[j] - Y[i];
        var d2 = dx * dx + dy * dy + SOFT2;
        var inv = 1 / Math.sqrt(d2);
        var f = G * inv * inv * inv;          // f = G / d³ — dx·f が加速度成分
        AX[i] += dx * f; AY[i] += dy * f;
        AX[j] -= dx * f; AY[j] -= dy * f;
      }
    }
    for (var i = 0; i < N; i++) {
      VX[i] += AX[i] * DT; VY[i] += AY[i] * DT;
      X[i] += VX[i] * DT;  Y[i] += VY[i] * DT;
    }
  }

円盤の縁が円軌道速度になる播種

円盤の中心質量は R² に比例する値を置き、縁の星が円軌道に乗る速度 v = √(M/r) を星ごとに与えます。この盤の累積面積20.125%は最も狭い数値ですが、尾が一瞬で大きく伸びる場面の性格そのものです。

02渦巻き銀河の形成 — 円盤の星が自然に腕を作る過程

静止円盤に描かない中心質量 MHALO=6200 の引力と星同士の重力を足し、積分器をleapfrogにすると、内側が速く回る差動回転が初期の密度ゆらぎを巻き上げて渦巻き腕が明瞭になります。METHOD定数を'euler'に変える変形比較の土台でもあり、3.0秒ループでフレーム最大24.639%は巻き戻しの瞬間に残像が一斉にずれる区間の値です。

渦巻き銀河の形成 — 円盤の星が自然に腕を作る過程
新しいタブで開く

02 · 渦巻き銀河の形成 — 円盤の星が自然に腕を作る過程 — 計測 累積面積 31.31% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DfillRectrequestAnimationFrame

leapfrogでエネルギーを保存する

加速を前後の半ステップで対称に計算するleapfrog(速度verlet)は、円盤が何周しても膨らまない積分器です。a(t)を再利用して1ステップの加速計算を2回に抑えます。

  function stepLeap() {
    // leapfrog(速度verlet) — a(t) を再利用して半分ずつ対称に押し、エネルギーを保存する
    if (!aReady) { accel(); aReady = true; }
    var h = DT * 0.5;
    for (var i = 0; i < N; i++) {
      VX[i] += AX[i] * h; VY[i] += AY[i] * h;   // v(t+h/2) = v(t) + a(t)·h/2
      X[i]  += VX[i] * DT; Y[i] += VY[i] * DT;  // x(t+h)   = x(t) + v(t+h/2)·h
    }
    accel();                                     // a(t+h)
    for (var i = 0; i < N; i++) {
      VX[i] += AX[i] * h; VY[i] += AY[i] * h;   // v(t+h) = v(t+h/2) + a(t+h)·h/2
    }
  }

中心質量と星の質量の区別

MHALO=6200は画面に描かれない核とハローの合計で、星それぞれの質量は0.5〜1.3の範囲の配列に収めます。銀河 形成 シミュレーション の核心は、この2種類の質量を足した引力の中で差動回転がゆらぎを増幅する点にあります。

03球状星団の公転 — 数百の星が共通重心を回る軌道

円盤ではなく球面に撒いた星280個がどう回るかを見る盤で、視線方向のzも計算に入り、画面はFOCAL=420の透視投影にすぎません。角度がばらばらなので差動回転では腕ができず、星は毎フレーム再計算した重心の周りをそれぞれの軌道面で回り、星団全体がゆっくり回ります。SPIN=0.55の集団回転を与え、G=26・ε²=60で3次元の全ペアを直接計算します。

球状星団の公転 — 数百の星が共通重心を回る軌道
新しいタブで開く

03 · 球状星団の公転 — 数百の星が共通重心を回る軌道 — 計測 累積面積 22.827% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DarcrequestAnimationFrame

3次元の全ペア重力

2次元の盤と同じペアループにdzを1本足すだけで3次元のn体になります。画面の奥行きは投影の問題で、計算そのものはx・y・zを平等に扱います。

  function step() {
    // 全ペア O(N²) 3次元重力 — a_i = Σ G·(x_j - x_i)/(|d|²+ε²)^1.5 (近似なし)
    var i, j, dx, dy, dz, d2, inv, f;
    for (i = 0; i < N; i++) { AX[i] = 0; AY[i] = 0; AZ[i] = 0; }
    for (i = 0; i < N; i++) {
      for (j = i + 1; j < N; j++) {
        dx = X[j] - X[i]; dy = Y[j] - Y[i]; dz = Z[j] - Z[i];
        d2 = dx * dx + dy * dy + dz * dz + SOFT2;
        inv = 1 / Math.sqrt(d2);
        f = G * inv * inv * inv;
        AX[i] += dx * f; AY[i] += dy * f; AZ[i] += dz * f;
        AX[j] -= dx * f; AY[j] -= dy * f; AZ[j] -= dz * f;
      }
    }
    for (i = 0; i < N; i++) {
      VX[i] += AX[i] * DT; VY[i] += AY[i] * DT; VZ[i] += AZ[i] * DT;
      X[i] += VX[i] * DT;  Y[i] += VY[i] * DT;  Z[i] += VZ[i] * DT;
    }
  }

球の等密度播種と接線速度

半径はr³で一様に引き、速度は位置に垂直な接線方向からランダムに選ぶので、軌道面が星ごとに別々になります。円盤のように整列しない星団の核となる性質です。

04ダークマターハロー — 見えない質量を入れると軌道が変わる実験

九つの盤で唯一の近似で、星同士の重力を全ペアで計算せず、毎ステップ星の質量を半径ビン(格子K=28)に累積して球対称の内包質量M(r)を作り、各星は自分の半径の内側の質量ぶんだけ中心へ引かれます。トグルで見えない質量プロファイル ρ ∝ 1/(r²+rc²) を同じ格子に上乗せすると外側の星の円軌道速度が保たれ、6.0秒ループで累積面積37.479%は九つで最も広い塗りです。

ダークマターハロー — 見えない質量を入れると軌道が変わる実験
新しいタブで開く

04 · ダークマターハロー — 見えない質量を入れると軌道が変わる実験 — 計測 累積面積 37.479% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

ハローの質量プロファイルを格子に積分する

ハローの質量は星と衝突しない固定分布として先に計算し、HALOBOOST=2.6倍を星の総質量に掛けて格子に配ります。rcは画面の短辺の0.15倍です。

  function buildHalo() {
    // ハロープロファイル — 固定分布(星と衝突しない冷たいダークマターの想定)。
    // ρ ∝ 1/(r²+rc²) を格子ビンに積分して収めておく(1回だけ計算)
    var rc = Math.min(W, H) * 0.15;
    var mstar = 0;
    for (var i = 0; i < N; i++) mstar += MASS[i];
    var raw = [];
    for (var k = 0; k < K; k++) {
      var r0 = rEdge(k), r1 = rEdge(k + 1);
      raw.push((Math.atan(r1 / rc) - Math.atan(r0 / rc)) * rc);  // ∫ r²/(r²+rc²) dr の近似形
    }
    var sum = 0;
    for (k = 0; k < K; k++) sum += raw[k];
    for (k = 0; k < K; k++) haloMass[k] = raw[k] / sum * mstar * HALOBOOST;
  }

方向情報を捨てる代償

この近似は星を中心方向へだけ引く等方近似なので、潮汐尾のような横に引かれる構造は再現できません。回転曲線という1つの目的のために方向の情報を捨てる盤で、ほかの8つが全ペアの直接計算である理由がここにあります。

05合体残骸 — 渦巻きが砕けて丸くなる結末

始まりは最初の接近ではなく2回目の直前で、シードで2つの円盤を近くに置き、PREPASS=420ステップを先に積分して、最初のすれ違いと尾の形成が済んだ状態から始めます。画面の時間が流れると軌道の向きが混ざって渦巻き構造が散らばり、特徴のない滑らかな楕円体が残る盤で、全ペアO(N²)とleapfrogは01と同じ近似なしの計算です。フレーム最大4.899%は2度目以降の乱れが穏やかな性格の表れです。

合体残骸 — 渦巻きが砕けて丸くなる結末
新しいタブで開く

05 · 合体残骸 — 渦巻きが砕けて丸くなる結末 — 計測 累積面積 32.382% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DellipserequestAnimationFrame

2回目の接近から始めるPREPASS

最初の接近を飛ばすには、シードの直後に420ステップを黙って積分するだけです。時刻t=0をどこに置くかの選択だけで、合体の結末が主題の盤として成立します。

  function seed() {
    // 2つの円盤を最初のすれ違い直前の距離(近め)に置き、PREPASS ステップを先に積分 —
    // 画面の t=0 は、すでに一度すれ違って尾が並んだ2回目の接近直前の状態
    X = []; Y = []; VX = []; VY = []; N = 0; aReady = false;
    var rnd = mulberry32(9);
    var half = NSTARS >> 1;
    var R = Math.min(W, H) * 0.44;
    var sc = R;
    placeDisk(W * 0.36, H * 0.34, R, half, 0, 0.62, -0.42, 1.0, rnd, sc);
    placeDisk(W * 0.64, H * 0.66, R, NSTARS - half, Math.PI, -0.62, 0.42, 0.85, rnd, sc);
    for (var s = 0; s < PREPASS; s++) stepLeap();
  }

軌道の向きが混ざる結末

すれ違いを2度経験した星の軌道は向きが入り混じり、渦巻きの特徴が消えた楕円体に落ち着きます。合体残骸が楕円銀河であるという説明の再現です。

06潮汐尾のクローズアップ — ある1星の軌跡で見る放出

計算は01と同じ星340個の全ペアO(N²)とleapfrogで、変わるのは描き方だけです。他の星は薄い点と短い残像にして、円盤の外側に植えた追跡星(TRACK)1個の座標だけをTRAILN=180のリングバッファに太いアンバーの軌跡として積みます。2つの中心の間を行き来しながら潮汐の蹴りで放り出される個別軌道が1本の線で見える盤です。

潮汐尾のクローズアップ — ある1星の軌跡で見る放出
新しいタブで開く

06 · 潮汐尾のクローズアップ — ある1星の軌跡で見る放出 — 計測 累積面積 20.762% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DlineTorequestAnimationFrame

古いほど薄くなる軌跡のフェード

リングバッファの座標を古い順に線で繋ぎ、透明度を最新のセグメントほど濃くします。1本の線の濃淡が時間の流れをそのまま運びます。

    // 追跡軌跡 — 太いアンバーの線。古いセグメントほど薄く(フェード)
    var tn = trailX.length;
    if (tn > 1) {
      for (var s = 1; s < tn; s++) {
        var age = s / tn;                    // 0 古い → 1 最新
        ctx.strokeStyle = rgba(C_HOT, 0.10 + age * 0.75);
        ctx.lineWidth = 0.8 + age * 1.9;
        ctx.beginPath();
        ctx.moveTo(trailX[s - 1], trailY[s - 1]);
        ctx.lineTo(trailX[s], trailY[s]);
        ctx.stroke();
      }
    }

統計ではなく1つの物語

尾を数字の変化ではなく個別の軌道として見ると、2つの中心の間の往復と放出が同じ線に刻まれます。累積面積20.762%は追跡対象が1個であることの直接の結果です。

07彗星を撃つ — ドラッグの角度と強さで星を投げ込む

円盤銀河(描かない中心質量 MHALO=5400 と星180個)の舞台に、ドラッグで引っ張った反対方向へスリングショット発射する盤です。彗星は配列の末尾に追加され、特別扱いなしで同じ全ペアの重力場に入り、角度と速度次第で楕円軌道に捕まったり双曲線で脱出したりします。LAUNCHV=0.55の倍率が発射速度を決め、描画では6発が決まった角度で自動発射されます。

彗星を撃つ — ドラッグの角度と強さで星を投げ込む
新しいタブで開く

07 · 彗星を撃つ — ドラッグの角度と強さで星を投げ込む — 計測 累積面積 26.731% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pointerdownpointermoverequestAnimationFrame

ドラッグの3つのポインターイベント

押した点と離した点の距離と向きがそのまま発射の強さと方向になります。長さは2.2倍して上限260で抑え、6px未満のずれは無視します。

  field.addEventListener('pointerdown', function (e) {
    var p = ptr(e);
    aim = { x0: p.x, y0: p.y, x1: p.x, y1: p.y };
    aimLbl.hidden = false;
    aimLbl.textContent = 'ドラッグで発射';
    e.preventDefault();
  });
  field.addEventListener('pointermove', function (e) {
    if (!aim) return;
    var p = ptr(e);
    aim.x1 = p.x; aim.y1 = p.y;
  });
  field.addEventListener('pointerup', function (e) {
    if (!aim) return;
    var p = ptr(e);
    var dx = aim.x0 - p.x, dy = aim.y0 - p.y;       // 引いた反対 = 発射方向
    var len = Math.hypot(dx, dy);
    if (len > 6) {
      var sp = Math.min(260, len * 2.2) * LAUNCHV;
      addComet(aim.x0, aim.y0, dx / len * sp, dy / len * sp, simSteps);
    }
    aim = null; aimLbl.hidden = true;
  });
  field.addEventListener('pointerleave', function () { aim = null; aimLbl.hidden = true; });

重力が本物である手での確かめ方

同じ計算の中に手で投げた星が入るので、楕円に捕まるか脱出するかは初速だけで決まります。累積面積26.731%・強度55.3は九つの盤の中でも安定した帯です。

08軌跡ヒストリー — 残像で積もる星の過去

01〜07の残像が毎フレーム少しずつ消えていたのに対し、この盤は減衰をなくして過去フレームをglobalAlphaで重ねて敷くだけです。軌道が積み続けられ、渦巻き腕が1枚の腕ではなく数百の軌道の累積であることが時間とともに見えてきます。計算は02と同じ全ペアO(N²)と中心質量 MHALO=6200 のleapfrogで、強度平均62.6は九つで最も明るい数値です。

軌跡ヒストリー — 残像で積もる星の過去
新しいタブで開く

08 · 軌跡ヒストリー — 残像で積もる星の過去 — 計測 累積面積 32.485% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

消さずに重ねるLAYERS枚の描画

ステップごとにglobalAlphaを低くした描画をLAYERS=3枚重ねると、軌道が線として固まります。内側の18%の半径は hot 色、外側は glow 色で塗り分けます。

  function stamp() {
    // 累積 — ステップごとに globalAlpha を低くして何枚も重ねると軌道が線として固まる
    if (prev) {
      for (var L = 0; L < LAYERS; L++) {   // LAYERS 枚 — 積み上がる速さの変形
        chainCtx.globalAlpha = 0.22 / LAYERS * (L + 1);
        for (var i = 0; i < N; i++) {
          var r = Math.hypot(X[i] - W * 0.5, Y[i] - H * 0.5);
          var col = r < Math.min(W, H) * 0.18 ? C_HOT : C_GLOW;
          chainCtx.strokeStyle = rgba(col, 0.9);
          chainCtx.lineWidth = r < Math.min(W, H) * 0.18 ? 2.6 : 2.1;
          chainCtx.beginPath();
          chainCtx.moveTo(prev[0][i], prev[1][i]);
          chainCtx.lineTo(X[i], Y[i]);
          chainCtx.stroke();
        }
      }
      chainCtx.globalAlpha = 1;
    }
    if (!prev) { prev = [X.slice(), Y.slice()]; }
    else { prev[0] = X.slice(); prev[1] = Y.slice(); }
  }

腕は軌道の密度である証拠

積もった軌道の文様と02で成長する腕を並べると、腕が星を縛る構造ではなく軌道が重なる場所であることが数値でも裏付けられます。5.0秒のループはこの積み上がりを見せるための長さです。

09ディープフィールドシーン — 何億年も込めた銀河進化の舞台

createRadialGradientで別キャンバスに焼いた星雲スプライト3枚(ターコイズ・ローズ・アンバー)を毎フレーム重ね、その上に3つの銀河(星110+110+110、銀河ごとに違うパステル3色)を対角線で交差する軌道に植えます。すれ違い→潮汐尾→合体(楕円化)の順の進化が1ループの中で現れる締めの盤で、星雲は毎フレーム作り直さず焼いた1枚を載せるだけです。

ディープフィールドシーン — 何億年も込めた銀河進化の舞台
新しいタブで開く

09 · ディープフィールドシーン — 何億年も込めた銀河進化の舞台 — 計測 累積面積 30.189% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createRadialGradientCanvasRenderingContext2DrequestAnimationFrame

3つの円盤を対角線に植える

円盤ごとに接近速度と回転の向きを変え、色分け配列GIDで3色を割り当てます。中心質量のない散在星を混ぜて軌跡が画面の隅まで伸びるようにします。

  function placeDisk(cx, cy, R, count, phase, dvx, dvy, dir, gid, rnd) {
    var M = R * R * 0.90;
    for (var i = 0; i < count; i++) {
      var r = R * Math.sqrt(rnd());
      var a = phase + (i / count) * Math.PI * 2 + rnd() * 0.5;
      var v = Math.sqrt(M / Math.max(r, 6)) * dir;
      X.push(cx + Math.cos(a) * r);
      Y.push(cy + Math.sin(a) * r);
      VX.push(dvx * R * 0.30 + -Math.sin(a) * v + (rnd() - 0.5) * 2);
      VY.push(dvy * R * 0.30 +  Math.cos(a) * v + (rnd() - 0.5) * 2);
      GID.push(gid);
      N++;
    }
  }

進化の順番が1ループに収まる理由

接近速度と初期距離の組み合わせで、1周4.5秒の間に2組のすれ違いと1度の合体が順に起きます。累積面積30.189%・強度52.2は星雲の下地を含まない星だけの値です。

ソフトニングを外すと1フレームで壊れる近接接触

2つの星が近づきすぎると加速度が爆発する

ニュートン重力は距離が0に近づくと発散するので、softeningなしでは2つの星が偶然重なった瞬間に1フレームで計算全体が画面の外へ飛びます。そこで全ての盤が距離の2乗にsofteningの2乗を足しており、その値が盤ごとに16〜90と異なるのは場面の性質の違いです。激しくすれ違う01・05・06は16と小さくして接近の加速を残し、星が長く留まる03は60と大きくして安定を先に確保します。

前進Eulerはエネルギーを注入する

01と03が使うsemi-implicit Eulerは1フレームの誤差こそ小さいものの、毎フレーム少しずつエネルギーを足したり奪ったりするので、数周回ると軌道が外へ膨らみます。2〜3秒のループでは目立ちませんが、長く回し続けると円盤が散ってしまうため、02と04〜09は半ステップを対称に押してエネルギーを保つleapfrogで積分します。02のMETHOD定数1行で両方を切り替えられるのは、この差を自分の目で確かめるためです。

彗星を配列に入れた瞬間にバッファの寸法がずれる

07でドラッグして新しい星を投げ込むと星数Nが増えますが、このとき加速度バッファのAXとAYは前の寸法のままです。再計算の前にこのバッファを参照すると未定義の値から始まってNaNが計算全体に広がるので、addCometは配列を伸ばした直後にaReadyを切って、次のステップでバッファを作り直させます。星を1つ足すことも状態を1つだけ触る作業ではない、という箇所です。

九つの盤の index.html と style.css が丸ごと入ったzipを展開するパスワードはhpcmbqutです。展開すれば本文に引用した関数がコメント込みでそのまま入っているので、行単位で突き合わせて確かめられます。

この9枚を実際どこに置くか — 表紙背景と体験型デモ

宇宙・科学系サイトの表紙なら09 ディープフィールドシーンをそのまま背景に敷くのが一番早い方法です。何億年もの進化をすでに1枚の構図に圧縮してあるので、見出しを重ねるだけでヒーローセクションが完成します。訪問者に実際に触ってほしい実験室やチュートリアルページには07 彗星を撃つを置くと、角度と強さをドラッグで決める操作がそのまま体験型デモになります。

軌道が積み重なる様子を見せたい物理教育コンテンツには08 軌跡ヒストリーが向いています。残像として積もる星の過去だけで、腕が固定された構造物ではないことを言葉なしに示せます。01 2つの銀河衝突はそれ自体が最も劇的な画面なので、ブログ記事や発表資料の導入カットとしてもそのまま使えます。

星の数と積分法を変える3つの変種とreduced-motion

名前 変更する値 変わる感触
星の数 01 NSTARS 340 → 120 統計の塊ではなく個別の軌道が浮かび上がる
すれ違う距離 01 SKIP 0.60 → 0.25 深く掠れば尾2本の代わりに核が貫通して散る
積分器 02 METHOD leapfrog → euler 数周で円盤が人工的に膨らむ差が目の前に出る

定数ブロックの1行を書き換えるだけ

3つとも書き換えるのはソース先頭の変数ブロックの1行だけです。星の数は構造の解像度、すれ違いの距離は場面の性格、積分器は安定と精度の関係という、それぞれ別の性格のつまみです。

動きを減らす設定で残る1枚

九つの盤すべてにprefers-reduced-motion: reduceの分岐が組み込まれており、ループを回さず接近が起きた場面まで積分した1枚を固定し、キャンバスに.is-stillクラスを付けます。静止していても潮汐尾や育った腕が見えるので、動きを止めた訪問者にも各盤の結論の場面が届きます。

触れる1枚と完結する8枚

07だけがポインター入力を受け付け、ほかの8つは手を触れなくても完結します。07の発射領域には照準の状態を伝えるラベルが付き、04のハローのトグルボタンはaria-pressedで状態を示すので、キーボードと読み上げでも扱えます。

なぜ星は自然と渦巻きになるのか — 重力積分と本物の渦巻きの違い

重力の大きさは距離の2乗、方向成分は3乗で割る

毎フレームの計算の核は、距離の2乗にsofteningを足して平方根を1回取るだけの4行です。画面座標で方向成分を足すには距離の3乗で割った商にdxを掛ける形になり、数値計算の仕組みを何も使わずに済みます。Gと質量は物理定数ではなくキャンバスのピクセルに合わせたシミュレーション値で、Gは26〜140まで盤ごとに、描かれない中心質量は5400〜6200の範囲で異なります。

差動回転がゆらぎを巻き上げて腕にする

02の円盤は内側ほど速く回る円軌道速度で播種されるので、初期の小さな密度ゆらぎが速度差で引き伸ばされながら巻き上がります。腕の本数と巻き方向はシードのゆらぎが決め、計算の途中で描き込まれるものではありません。シードが決定論的な乱数で固定されているので、読み込むたびに同じ腕が育ちます。

腕は固定された構造ではなく軌道が重なる場所

08のように軌道を積み続けると、同じ腕の場所を通る星が入れ替わり続ける様子が見えます。腕は星を縛っておく紐ではなく軌道が互いに近づいて密度が一時的に高くなる領域で、個々の星は腕を通り抜けて自分の軌道をそのまま回ります。

すれ違うだけでも尾が出る潮汐力の差

潮汐尾は衝突の瞬間の出来事ではなく、すれ違っている間に星ごとに受ける引力の差が積もった結果です。相手の銀河に近い側の星は強く、遠い側の星は弱く引かれるので円盤が両側に伸び、この伸びが軌道エネルギーに変わって外へ吹き出します。06の追跡軌跡が2つの中心の間を往復して投げ出される形なのは、この差が個別軌道に残す痕跡です。

この銀河シミュレーションを組み込む前によくある質問

この銀河 シミュレーション を自分のページに置くには何から変えればいいですか?

zipを展開すると九つの項目のフォルダーが出るので、好きな盤1つの index.html と style.css の2ファイルをフォルダーごとアップロードすればそのまま動きます。色はstyle.cssの--gx-系トークンで、場面の大きさは同じファイルの.gx__fieldの高さで調整します。どの盤も他の盤を参照しないので、1つだけ持ち出しても残りは無関係です。

重力 シミュレーション サイト風のページの1場面に使えますか?

使えます。09は表紙の hero 用に構図を決めた盤で、01と08は暗い色だけで塗るので背景に敷いても本文の読みやすさを損ないません。ただし重力の計算が毎フレーム走るので、低速な端末のために星の数を減らして置くほうが安全です。

星の数を大きく増やすとどこで遅くなりますか?

ペアの数は星の数の2乗で増え、01の340個は毎フレーム57,630ペアを計算しています。星を2倍にすればペアは4倍になるので、数千個を超えるあたりでフレームが落ち始め、それ以上を目指すなら04の格子質量のような近似に道を変える分かれ目になります。

もっと読む

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。