GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

은하 충돌 시뮬레이션 9종 — 서로 잡아당기는 별들로 나선팔·조석 꼬리를 만드는 캔버스

은하 충돌 시뮬레이션은 라이브러리·WebGL 없이 별 하나하나에 뉴턴 중력을 매 프레임 적분하면 두 은하가 스치며 조석 꼬리를 남기고 합쳐지는데, 새로고침 없이 눈앞에서 우주 규모의 인력이 이루어지는 장면을 그대로 복붙으로 가져갈 수 있습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

데모

아홉 판 각각은 index.html 과 style.css 두 파일로 자립해 열자마자 움직입니다. 24프레임 녹화 구간에서 아홉 판 전부 23프레임이 움직였고(움직인프레임 23), 별 하나에서 은하 전체까지 매 프레임 다시 계산되므로 화면의 어떤 별도 정지하지 않습니다. 07번은 드래그로 직접 별을 던져 넣을 수 있고 04번은 헤일로 토글을 버튼으로 고정할 수 있으니 지나가기 전에 만져 보시길 권합니다.

항목 초점 누적면적(%) 강도 평균
01 스침과 꼬리 두 원반이 스치며 꼬리를 뿜는 기본 장면 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 심우주 무대 세 은하가 차례로 스치는 대문 장면 30.189 52.2

누적면적은 04가 37.479%로 가장 넓고 01이 20.125%로 가장 좁습니다. 01·03·06·07은 별 점 자체가 화면의 좁은 몫을 차지하는 주제라 20~27% 대에 머무는데, 이는 장면의 성격 그대로이지 계산이 덜 일어난다는 뜻이 아닙니다. 프레임 변화의 강도는 08이 62.6으로 가장 컸고 02가 48.0으로 가장 낮았으며, 04의 우측 그래프에 그려지는 평평한 회전 속도는 우리 은하 시뮬레이션을 소개할 때마다 등장하는 바로 그 곡선입니다.

충돌 전부터 후까지, 시간축으로 늘어놓은 아홉 장면

순서는 하나의 충돌이 진행되는 시간을 축으로 잡았습니다. 01이 두 원반이 스쳐 꼬리를 뿜는 첫 순간을 맡고, 02는 스침 이전의 원반이 왜 팔을 갖게 되는지를 독립 장면으로 보여 줍니다. 03은 같은 중력으로도 원반이 아닌 구면 뭉치가 전혀 다르게 도는 걸 보이고, 04는 보이지 않는 질량을 얹는 토글 하나로 외곽 궤도를 바꿔 실험의 맛을 더합니다. 이 중 04만 계산 방식이 다릅니다 — 별 상호중력을 전 쌍으로 돌리는 대신, 매 스텝 별 질량을 반경 격자 28칸에 누적한 구면 누적 질량으로 등방 근사하고, 그래서 남는 프레임 예산을 우측 상단 회전 곡선 그래프에 썼습니다. 05는 첫 스침을 이미 지난 시점, 즉 시드에서 420스텝을 미리 적분한 상태에서 시작해 나선이 부서져 타원체로 굳는 결말을 보여 주고, 06은 같은 종류의 스침을 별 340개 전체가 아니라 추적 별 하나의 굵은 궤적으로 다시 봅니다. 07에서 드디어 손이 들어옵니다 — 드래그 각도와 세기로 새 별을 직접 던져 넣습니다. 08은 지워지지 않는 잔상으로 궤도의 누적을 보여 주고, 09는 세 은하와 성운 스프라이트를 깐 심우주 구도로 대문 장면을 닫습니다. 충돌 장면부터 보고 싶으면 01, 팔이 왜 생기는지가 궁금하면 02와 08이 곧장 답이 되는 순서입니다.

항목별 코드

01두 은하 충돌 — 스치며 조석 꼬리를 남기고 합쳐지는 기본 장면

두 은하 충돌 — 스치며 조석 꼬리를 남기고 합쳐지는 기본 장면
새 창에서 열기

01 · 두 은하 충돌 — 스치며 조석 꼬리를 남기고 합쳐지는 기본 장면 — 측정 누적면적 20.125% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DarcrequestAnimationFrame

별 340개(두 원반 각 170개)에 위치와 속도를 두고 모든 쌍의 중력을 매 프레임 합산하면 두 디스크가 스치는 순간 바깥 별들이 뿜어져 나가는 조석 꼬리가 저절로 생기는데, 이것이 충돌 사진에서 보던 형태의 계산적 기원이자 히어로 배경·사이언스 도해에 바로 쓰는 기본 장면입니다. 측정은 누적면적 20.125%·강도평균 59.3입니다.

모든 쌍을 한 번씩만 훑는 대칭 합산

  function step() {
    // 전 쌍 중력 — a_i = Σ_j G·(x_j - x_i)/(|d|²+ε²)^1.5. 대칭이라 쌍당 한 번 계산해 양쪽에 반영
    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;
    }
  }

02나선 은하 형성 — 원반 별들이 저절로 팔을 만드는 과정

나선 은하 형성 — 원반 별들이 저절로 팔을 만드는 과정
새 창에서 열기

02 · 나선 은하 형성 — 원반 별들이 저절로 팔을 만드는 과정 — 측정 누적면적 31.31% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DfillRectrequestAnimationFrame

정지 원반에 중심 질량(그리지 않는 MHALO 6200) 인력과 별 상호중력만 주고 궤도 속도로 돌리면 안쪽이 빠르게 도는 차등회전이 초기 밀도 요동을 감아 올려 나선 팔이 저절로 선명해지는데, 이 판에서 적분기를 오일러 대신 에너지를 오래 보존하는 leapfrog으로 바꾼 로딩 씬·교육 도해용 장면입니다. 측정은 누적면적 31.31%·강도평균 48.0입니다.

중심 질량과 별 상호중력을 한 번에 모으는 가속도 합산

  function accel() {
    var i, j, dx, dy, d2, inv, f;
    for (i = 0; i < N; i++) { AX[i] = 0; AY[i] = 0; }
    // 중심 질량 — 원점이 아니라 원반 중심을 향한다(원반이 화면 중앙에 있으므로 W/2, H/2)
    var cx = W * 0.5, cy = H * 0.5;
    for (i = 0; i < N; i++) {
      dx = cx - X[i]; dy = cy - Y[i];
      d2 = dx * dx + dy * dy + SOFT2;
      inv = 1 / Math.sqrt(d2);
      f = G * MHALO * inv * inv * inv;
      AX[i] += dx * f; AY[i] += dy * f;
    }
    // 별 상호 중력 — 대칭이라 쌍당 한 번 계산해 양쪽에 반영
    for (i = 0; i < N; i++) {
      for (j = i + 1; j < N; j++) {
        dx = X[j] - X[i]; dy = Y[j] - Y[i];
        d2 = dx * dx + dy * dy + SOFT2;
        inv = 1 / Math.sqrt(d2);
        f = G * MASS[i] * MASS[j] * inv * inv * inv;
        AX[i] += dx * f; AY[i] += dy * f;
        AX[j] -= dx * f; AY[j] -= dy * f;
      }
    }
  }

03구상 성단 공전 — 수백 개 별이 무게중심을 도는 궤도

구상 성단 공전 — 수백 개 별이 무게중심을 도는 궤도
새 창에서 열기

03 · 구상 성단 공전 — 수백 개 별이 무게중심을 도는 궤도 — 측정 누적면적 22.827% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DarcrequestAnimationFrame

별 280개를 3차원 구면으로 흩뿌리고 접선 방향의 랜덤 궤도 속도를 주면 원반과 달리 중심으로 뭉치지 않고 제각각의 궤도면으로 무게중심 주위를 돌며 성단 전체가 느리게 뒤집히는데, 화면은 초점거리 420의 원근 투영일 뿐 계산 자체는 세 축에서 일어나는 우주 테마 배경·별자리 인터랙티브용 장면입니다. 측정은 누적면적 22.827%·강도평균 49.4입니다.

z 축까지 전 쌍으로 도는 3차원 순회

  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;
    }
  }

04암흑물질 헤일로 — 보이지 않는 질량을 넣자 궤도가 바뀌는 실험

암흑물질 헤일로 — 보이지 않는 질량을 넣자 궤도가 바뀌는 실험
새 창에서 열기

04 · 암흑물질 헤일로 — 보이지 않는 질량을 넣자 궤도가 바뀌는 실험 — 측정 누적면적 37.479% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

보이는 원반 바깥에 그리지 않는 질량 분포를 얹으면 외곽 별들의 회전 속도가 떨어지지 않고 유지되는데, 자동 루프가 3초마다 헤일로를 껐다 켜고 버튼으로 고정할 수도 있습니다. 아홉 판 중 이 판만 전 쌍 계산 대신 반경 격자의 구면 누적 질량으로 등방 근사해 남는 계산을 우측 회전 곡선 그래프에 쓰고, 측정은 누적면적 37.479%로 가장 넓고 강도평균 50.5입니다.

보이지 않는 질량 프로파일을 격자에 미리 구워 두는 준비

  function buildHalo() {
    // 헤일로 프로파일 — 고정 분포(별과 충돌하지 않는 차가운 암흑물질 가정).
    // ρ ∝ 1/(r²+rc²) 를 격자 빈에 적분해 담아 둔다(한 번만 계산)
    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;
  }

05합체 잔해 타원 은하 — 나선이 부서져 뭉툭해지는 결말

합체 잔해 타원 은하 — 나선이 부서져 뭉툭해지는 결말
새 창에서 열기

05 · 합체 잔해 타원 은하 — 나선이 부서져 뭉툭해지는 결말 — 측정 누적면적 32.382% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DellipserequestAnimationFrame

두 원반을 첫 스침 직전 거리에 놓고 420스텝을 미리 적분해 화면의 시작이 이미 한 번 스치고 꼬리가 늘어선 상태가 되게 하고, 시간이 더 흐르면 궤도 방향이 뒤섞여 나선팔이 흩어지고 특징 없는 타원체가 남는 스토리텔링 씬·우주 콘텐츠 히어로의 결말 장면입니다. 측정은 누적면적 32.382%·강도평균 48.4입니다.

시작 장면을 만들기 위해 미리 흘려 두는 사전 적분

  function seed() {
    // 두 원반을 첫 스침 직전 거리(가깝게)에 놓고 PREPASS 스텝 미리 적분 —
    // 화면의 t=0 은 이미 한 번 스치고 꼬리가 늘어선 두 번째 스침 직전 상태다
    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();
  }

06조석 꼬리 클로즈업 — 별 하나의 궤적으로 보는 뿜어짐

조석 꼬리 클로즈업 — 별 하나의 궤적으로 보는 뿜어짐
새 창에서 열기

06 · 조석 꼬리 클로즈업 — 별 하나의 궤적으로 보는 뿜어짐 — 측정 누적면적 20.762% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DlineTorequestAnimationFrame

수백 별 중 원반 가장자리의 딱 한 별만 굵은 앰버 궤적을 남기게 하면 그 별이 두 은하 중심 사이를 왕복하다 조석 킥에 바깥으로 던져지는 개별 궤도가 드러나는데, 궤적은 180프레임짜리 링 버퍼에 쌓아 오래된 세그먼트일수록 옅게 지워가는 데이터 스토리텔링·강조 애니메이션용 레이어입니다. 측정은 누적면적 20.762%·강도평균 55.0입니다.

오래된 궤적일수록 옅어지는 세그먼트 페이드

    // 추적 궤적 — 굵은 앰버 라인. 오래된 세그먼트일수록 옅게(페이드)
    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();
      }
    }

07혜성 쏘기 — 드래그 각도·세기로 별을 직접 던져 넣기

혜성 쏘기 — 드래그 각도·세기로 별을 직접 던져 넣기
새 창에서 열기

07 · 혜성 쏘기 — 드래그 각도·세기로 별을 직접 던져 넣기 — 측정 누적면적 26.731% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointerdownpointermoverequestAnimationFrame

원반 은하가 있는 캔버스를 드래그해 당긴 방향의 반대로 새 별을 던져 넣으면 각도에 따라 궤도에 잡히기도 그대로 탈출하기도 하는, 중력이 진짜 계산이라는 걸 손으로 확인하는 대표 인터랙션입니다. 조준은 pointerdown·pointermove·pointerup 세 단계로 이루어지고 측정은 누적면적 26.731%·강도평균 55.3입니다.

당긴 반대 방향으로 쏘는 포인터업 처리

  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;
  });

08궤적 히스토리 — 잔상으로 쌓이는 별의 과거

궤적 히스토리 — 잔상으로 쌓이는 별의 과거
새 창에서 열기

08 · 궤적 히스토리 — 잔상으로 쌓이는 별의 과거 — 측정 누적면적 32.485% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

01부터 07까지의 잔상이 매 프레임 조금씩 지워진 것과 달리 이 판은 아예 지우지 않고 globalAlpha를 낮게 해 여러 겹씩 깔아 궤도를 선으로 굳히는데, 나설팔이 한 장의 스냅샷이 아니라 궤도의 누적임을 보여 주는 히어로 배경·제너러티브 아트용 레이어입니다. 측정은 누적면적 32.485%·강도평균 62.6으로 강도는 아홉 판 중 가장 높습니다.

지우지 않고 겹겹이 쌓는 잔상 스탬프

  // decay 가 없다 — 잔상 체인은 지워지지 않고 계속 쌓인다(이 데모의 주제).
  // 대신 루프 되감을 때만 통째로 지운다.

  var prev = null;
  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(); }
  }

09심우주 씬 — 몇 억 년을 담은 은하 진화 무대

심우주 씬 — 몇 억 년을 담은 은하 진화 무대
새 창에서 열기

09 · 심우주 씬 — 몇 억 년을 담은 은하 진화 무대 — 측정 누적면적 30.189% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createRadialGradientCanvasRenderingContext2DrequestAnimationFrame

어두운 배경에 방사형 그라디언트로 구운 성운 스프라이트 세 장을 깔고 세 은하와 산개 별 84개를 대각선 궤도로 놓으면 두 쌍이 스치고 셋째 은하가 합류하는 장면이 이어지는, 심우주 사진의 구도를 한 캔버스로 옮긴 대문 히어로이자 스플래시 화면입니다. 측정은 누적면적 30.189%·강도평균 52.2입니다.

성운 한 장을 방사형 그라디언트로 미리 구워 두기

  function bakeNebulas() {
    nebCvs = [];
    var rnd = mulberry32(77);
    var cols = [C_GLOW, C_TAIL, C_HOT];
    for (var k = 0; k < NEBULA; k++) {
      var s = document.createElement('canvas');
      var sz = Math.max(160, Math.round(Math.min(W, H) * (0.55 + rnd() * 0.5)));
      s.width = sz; s.height = sz;
      var sc = s.getContext('2d');
      var g = sc.createRadialGradient(sz * 0.5, sz * 0.5, 0, sz * 0.5, sz * 0.5, sz * 0.5);
      var c = cols[k % 3];
      g.addColorStop(0.00, rgba(c, 0.16));
      g.addColorStop(0.35, rgba(c, 0.07));
      g.addColorStop(0.70, rgba(c, 0.02));
      g.addColorStop(1.00, rgba(c, 0));
      sc.fillStyle = g;
      sc.fillRect(0, 0, sz, sz);
      nebCvs.push({ cv: s, x: 0.12 + rnd() * 0.76, y: 0.10 + rnd() * 0.80, sz: sz });
    }
  }

대문 히어로와 인터랙티브 랩 페이지 — 이 아홉 개를 어디에 앉히나

우주·과학 테마 사이트의 대문이라면 09 심우주 씬을 그대로 배경에 앉히는 것이 가장 빠릅니다. 몇 억 년의 진화를 이미 한 장면에 눌러 담아 둔 구도라 타이포만 얹으면 히어로 섹션이 완성됩니다. 방문자가 직접 손대 보길 바라는 실험실·튜토리얼 페이지에는 07 혜성 쏘기를 앉히면, 드래그 각도와 세기로 별을 던져 넣는 입력이 그대로 체험형 데모가 됩니다.

물리 교육 콘텐츠에서 궤도의 누적을 보여주고 싶다면 08 궤적 히스토리가 맞습니다. 잔상으로 쌓이는 별의 과거가 팔이 왜 고정된 구조물이 아닌지를 말로 설명하지 않아도 보여 줍니다. 01 두 은하 충돌은 그 자체로 가장 극적인 화면이라 블로그 글이나 발표 자료의 인트로 컷으로도 바로 씁니다.

소프트닝 없이 두 별이 겹치면 가속도가 폭발하는 함정

두 별이 지나치게 가까워지면 가속도가 폭발한다

뉴턴 중력은 거리가 0에 가까워지면 발산하므로, 소프트닝 없이는 두 별이 우연히 겹쳤을 때 그 프레임 하나로 계산 전체가 튕겨 나갑니다. 그래서 모든 판이 거리 제곱에 소프트닝 제곱을 더하는데, 그 값이 판마다 16~90으로 다른 것은 장면의 성격 차이입니다. 격하게 스치는 01·05·06은 16으로 작게 잡아 근접 가속을 살리고, 별이 오래 붙어 도는 03은 60으로 크게 잡아 안정을 먼저 확보합니다.

오일러 적분은 에너지를 흘려 보낸다

01이 쓰는 semi-implicit Euler는 한 프레임 오차가 작아 보여도 매 프레임 조금씩 에너지를 더하거나 빼앗아, 몇 바퀴를 돌면 궤도가 저절로 부풀어 오릅니다. 2초짜리 루프 안에서는 눈에 안 걸리지만 같은 계산을 오래 두고 돌리면 원반이 흩어지므로, 02와 04~09는 절반 스텝을 대칭으로 밀어 에너지를 보존하는 leapfrog으로 적분합니다. 03은 성단 별 각각의 느린 왕복 궤도가 주인공이라 짧은 루프에서 오일러로도 충분해 01과 같은 적분기를 유지했습니다. 두 적분기를 한 줄 상수로 갈아 끼울 수 있게 해 둔 것이 02의 변형 스위치입니다.

혜성을 배열에 넣는 순간 버퍼 크기가 어긋난다

07에서 드래그로 새 별을 던져 넣으면 별 수 N이 늘어나는데, 이때 가속도 버퍼 AX와 AY는 이전 크기 그대로입니다. 재계산 전에 이 버퍼를 참조하면 정의되지 않은 값에서 시작해 NaN이 계산 전체로 퍼지므로, addComet은 배열을 늘리는 즉시 aReady를 끄어 다음 스텝에서 버퍼를 다시 만들게 합니다. 별 하나를 추가하는 일도 상태 하나만 건드는 일이 아니라는 지점입니다.

아홉 판의 index.html 전문과 항목별 소스는 암호 zip 으로 묶여 있고, 여는 비번은 hpcmbqut 입니다. 풀면 본문에 인용한 함수들이 주석까지 그대로 들어 있어 줄 단위로 나란히 대조해 보실 수 있습니다.

왜 별들이 저절로 나선이 되나? — 중력 적분과 진짜 나선의 차이

중력의 세기는 거리 제곱, 방향 성분은 세제곱 나누기

이 중력 시뮬레이션에서 매 프레임 일어나는 계산은 01의 코드 그대로 네 줄입니다. 거리의 역제곱으로 힘의 크기가 정해지지만 화면 좌표로 방향 성분을 더하려면 거리 세제곱으로 나눈 몫에 dx를 곱하는 꼴이 되고, d제곱에 소프트닝 값을 더한 뒤 제곱근을 딱 한 번 나누는 것으로 끝나므로 수학 라이브러리가 필요 없습니다. G와 질량은 물리 상수가 아니라 캔버스 픽셀에 맞춘 시뮬레이션 값이라, G는 판마다 26~140으로, 그리지 않는 중심 질량은 5400~6200으로 다르게 잡았습니다.

차등회전이 잔물결을 감아서 팔을 만든다

02의 원반은 모든 별이 같은 각속도로 도는 게 아니라 안쪽이 더 빨리 도는 궤도 속도로 시드됩니다. 시작 시점의 작은 밀도 요동이 이 속도 차로 감겨 올라가면서 나선이 되는데, 팔의 개수와 감기는 방향은 시드의 요동이 정하지 계산 도중에 임의로 그려지지 않습니다. 시드가 결정론 난수로 고정되어 있어 다시 불러도 같은 팔이 자라는 것도 이 때문입니다.

팔은 고정된 구조물이 아니라 궤도가 겹치는 곳

08처럼 궤도를 계속 쌓아 보면 같은 팔 자리를 지나는 별들이 계속 바뀌는 걸 볼 수 있습니다. 팔은 별들을 묶어 둔 끈이 아니라 궤도가 서로 가까워져 잠깐 밀도가 높아지는 영역이며, 개별 별은 팔을 통과해 자기 궤도를 그대로 돕니다. 02와 08을 나란히 놓으면 계산으로 자라는 팔과 궤도의 누적으로 쌓이는 팔이 같은 현상의 두 얼굴임이 보입니다.

스치기만 해도 꼬리가 나오는 조석력의 차이

조석 꼬리는 두 은하가 부딪히는 순간의 사건이 아니라 스치는 동안 별마다 받는 인력의 차이가 쌓인 결과입니다. 상대 은하에 가까운 쪽 별은 강하게, 먼 쪽 별은 약하게 끌리므로 원반이 양쪽으로 늘어나고, 이 늘어남이 궤도 에너지로 바뀌며 바깥으로 뿜어져 나갑니다. 06의 추적 궤적이 두 중심 사이를 왕복하다 던져지는 모양을 보여 주는 것이 바로 이 차이가 개별 궤도에 남기는 흔적입니다.

별 개수와 적분기를 바꾸는 세 변형과 접근성

값 하나를 바꾼 세 변형

이름 바꾼 값 느낌
별 개수 01 NSTARS 340 → 120 통계적 덩어리 대신 개별 궤도가 드러난다
스치는 거리 01 SKIP 0.60 → 0.25 깊이 스치면 꼬리 두 개 대신 핵이 관통하며 폭발적으로 흩어진다
적분기 02 METHOD 'leapfrog' → 'euler' 오일러는 에너지가 흘러 몇 바퀴 뒤 원반이 부풀어 오른다

세 변형 모두 계산 구조를 건드리지 않고 상수 하나만 바꿉니다. 별 수를 줄이면 06의 추적 궤적처럼 개별 궤도가 눈에 들어오고, 스치는 거리를 좁히면 장면의 성격 자체가 바뀌며, 적분기를 내리면 안정과 정확의 관계를 손으로 확인할 수 있습니다.

동작을 줄이도록 설정한 사용자에게 주는 장면

각 판은 시작할 때 matchMedia('(prefers-reduced-motion: reduce)') 일치 여부를 검사해, 켜져 있으면 장면 중반까지 시뮬레이션을 미리 돌린 뒤 구조가 가장 잘 잡힌 한 장을 고정합니다. 01은 꼬리가 가장 그럴듯한 순간을, 02는 팔이 자란 순간을 고정하므로 동작을 줄인 사용자에게도 각 판의 결론 장면이 그대로 전달됩니다.

손으로 만지는 한 판과 나머지 여덟 판

07만 포인터 입력을 받고 나머지 여덟 판은 손을 대지 않아도 완결됩니다. 07의 발사 영역에는 조준 상태를 알리는 라벨이 붙어 화면 낭독기로 조작 흐름을 따라갈 수 있고, 04의 헤일로 고정 버튼도 키보드로 누를 수 있습니다.

은하 시뮬레이션을 페이지에 얹기 전 자주 묻는 것

이 데모를 제 페이지에 얹으려면 무엇부터 바꿔야 하나요?

zip 을 펼치면 항목 아홉 개의 폴더가 나오니 마음에 드는 판 하나의 index.html 과 style.css 두 파일만 폴더째 페이지에 올리면 그대로 움직입니다. 색은 style.css 의 --gx- 계열 팔레트 토큰을, 장면 크기는 같은 파일의 .gx__field 높이를 바꿔 조정합니다. 어떤 판도 다른 판을 참조하지 않으므로 하나만 가져다 써도 나머지와 무관합니다.

우주 시뮬레이션 사이트처럼 꾸민 페이지에 이 장면을 올려도 되나요?

됩니다. 09는 대문 히어로용으로 구도를 잡은 판이고 01과 08은 배경으로 깔아도 본문 가독을 해치지 않는 어두운 팔레트입니다. 다만 중력 계산이 매 프레임 돌므로 저사양 기기를 위해 별 수를 줄여 두는 편이 안전합니다.

별 수를 크게 늘리면 어느 시점에서 느려지나요?

쌍의 개수는 별 수의 제곱으로 늘어, 01의 별 340개가 매 프레임 57,630쌍을 계산하는 데 별을 두 배로 불리면 쌍은 네 배로 불어납니다. 수천 개를 넘기면 프레임이 떨리기 시작하니, 그 이상을 원하면 04의 격자 누적 질량 방식처럼 계산을 근사로 바꾸는 갈림길입니다.

더 읽을거리

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.