Skip to main content
Computer Science Three-Body Problem Examples

Three-Body Problem

Chaotic dance of celestial mechanics

ENGINE

Verlet
Symplectic, stable
RK4
4th order, accurate
Euler
1st order, fast

CONTROLS

© 2013 - 2026 Cylian 🤖 Claude
Instructions Claude

Prompt utilisé pour régénérer cette page :

Page: Three-Body Problem - Chaotic Celestial Mechanics Simulation
Title: "Three-Body Problem"
Description: "Chaotic dance of celestial mechanics"
Icon: triangle-outline
Tags: physics, simulation, visualization, celestial-mechanics
Category: computer-science
Front matter: js: default (no scss key)

HTML structure in index.md:
  <section class="container visual size-1000 ratio-1-1 canvas-contain">
    <canvas id="simulation-canvas"></canvas>
  </section>

Widget files:
  _engine.left.md (weight: 5, title: "Engine"):
    Inline <style> block for .engine-list styling (monospace 0.7rem, .engine-item with border-left, cursor pointer, .active state with --draw-color-primary border)
    div#engine-widget.engine-list with h4 "ENGINE" and 3 clickable engine items:
      div.engine-item[data-engine="verlet"]: .name "Verlet" + .desc "Symplectic, stable"
      div.engine-item[data-engine="rk4"]: .name "RK4" + .desc "4th order, accurate"
      div.engine-item[data-engine="euler"]: .name "Euler" + .desc "1st order, fast"

  _stats.left.md (weight: 10, title: "Stats"):
    Inline <style> for .body-stats (monospace 0.7rem, .body-stat with border-left 3px, .center-mass with #f1c40f border)
    div#body-stats.body-stats — populated dynamically by JS with h4 "BODIES", CoM position, per-body stats (mass, velocity, distance from CoM, escaping warning)

  _controls.right.md (weight: 10, title: "Controls"):
    Inline <style> for .controls-widget (monospace 0.7rem, .control-item with border-left 3px)
    div.controls-widget with h4 "CONTROLS":
      Checkbox: <input type="checkbox" id="show-trails" checked> Trails
      Speed slider: id="speed-slider" min=0.1 max=3 step=0.1 value=1, display span#speed-value "1.0"x
      Energy display: span#energy-display (populated dynamically)
      Buttons div.buttons (flex, gap 0.5rem, center):
        {{< button id="btn-play" label="Play" >}}
        {{< button id="btn-reset" label="Reset" >}}

  _menu.menu.md (weight: 10, title: "Menu"):
    <a class="item button" data-modal="examples">
      {{< icon name="science" >}} Examples
    </a>

  examples.modal.md (weight: 10, title: "Examples"):
    Inline <style> for .example-list (flex column, gap 0.5rem, max-height 60vh, overflow-y auto), .example-item (clickable, hover border), .example-section (section headers)
    div.example-list with 3 sections:
      Classical Solutions: Lagrange (1772), Euler (1767)
      Choreographies: Figure-8, Butterfly, Moth, Yarn, Yin-Yang, Dragonfly
      Demonstrations: Binary + Planet, Chaos, Random
    Each .example-item has data-preset attribute matching preset key, .title and .desc children

Architecture (6 JS files):
  default.js — Main orchestrator (IIFE, ES module imports, ~910 lines):
    Imports: Body from ./_physics-body.lib.js, EulerEngine from ./_physics-euler.lib.js, RK4Engine from ./_physics-rk4.lib.js, VerletEngine from ./_physics-verlet.lib.js, panic from /_lib/panic_v3.js, { createPresets, COLORS } from ./_preset.lib.js
    CONFIG: width=1000, height=1000, dt=1.2, tickInterval=16, trailFade=0.92
    PRESETS: created via createPresets(CONFIG.width, CONFIG.height)
    DOM elements: canvas (#simulation-canvas), presetSelect (#preset-select), engineSelect (#engine-select), btnPlay (#btn-play), btnReset (#btn-reset), showTrails (#show-trails), speedSlider (#speed-slider), speedValue (#speed-value), energyDisplay (#energy-display), bodyStatsWidget (#body-stats), engineWidget (#engine-widget), presetButton (querySelector '[data-modal="examples"]')
    State: bodies[], engine, running, animationId, initialEnergy, speedMultiplier=1.0, currentPreset='random', currentEngineType='verlet'
    Camera state: camera {x: 500, y: 500, zoom: 1.0}, isDragging, dragStart, cameraStart
    Body drag state: draggedBody, bodyDragOffset, hasDragged

    Coordinate conversion:
      worldToScreen(wx, wy): (wx - camera.x) * zoom + cx, (wy - camera.y) * zoom + cy
      screenToWorld(sx, sy): (sx - cx) / zoom + camera.x, (sy - cy) / zoom + camera.y
      getMousePos(e): Converts clientX/Y to canvas coords accounting for CSS scaling (CONFIG.width / rect.width)

    findBodyAt(sx, sy): Checks distance from screen position to each body (radius = max(4, sqrt(mass)*0.8) * min(zoom, 2)), hit detection at radius*1.5
    getCenterOfMass(): Returns {x, y, mass} weighted average of all bodies

    checkCollisions(): Merges bodies that overlap (dist < (radiusA+radiusB)*0.5). Conserves momentum: new position/velocity = mass-weighted average. Removes merged bodies in reverse order. Returns true if any collision occurred

    isEscaping(body): Energy criterion — body escapes if kinetic + potential > 0 (unbound) AND distance from CoM > CONFIG.width * 0.4

    createEngine(type): Factory returning EulerEngine/RK4Engine/VerletEngine with options {g: 1.0, softening: 2.0}

    Rendering:
      render(): Clears canvas, draws:
        1. Grid: 64px grid lines (rgba(255,255,255,0.05)), transformed through camera
        2. Trails: If showTrails checked, draws polyline for each body.trail in body.color+'60', lineWidth 1
        3. Bodies: For each body — escaping warning ring (dashed red circle at radius*3), velocity vector arrow (white, length min(40, speed*20)*zoom), radial gradient glow (color -> color+'80' -> transparent, radius*2), core circle (filled + white 1px stroke)
        4. Center of mass: Yellow cross (#f1c40f) + transparent circle
        5. Energy display: E value + drift percentage (green if <1%, red otherwise)
        6. Calls updateBodyStats()

    updateEngineWidget(activeEngine): Toggles .active class on .engine-item elements
    updatePresetButton(): Updates preset button text with capitalized preset name
    updateBodyStats(): Builds HTML in #body-stats — h4 "BODIES", CoM position, per-body: name, mass, velocity, distance from CoM, escaping indicator

    Simulation loop:
      loop(): requestAnimationFrame-based. Physics step: engine.step(bodies, dt * speedMultiplier). Checks collisions (recalculates initialEnergy on merge, stops if <=1 body). Records trails. Renders. Schedules next frame
      start(): Sets running=true, button text "Pause", calls loop()
      pause(): Sets running=false, button text "Play", cancelAnimationFrame

    initCanvas(): Sets canvas 1000x1000 (no DPR scaling)
    reset(): pause, get preset function, create bodies, create/reset engine, store initialEnergy, clear trails, reset camera to center/zoom=1, updateEngineWidget, updatePresetButton, initCanvas, render

    Mouse interaction:
      mousedown: Middle button or Shift+left → camera pan (isDragging). Left without shift when paused → body drag (draggedBody, records offset)
      mousemove: Camera pan updates camera.x/y. Body drag moves body position, clears trail
      mouseup: Ends drag. On body drag release: resets body velocity to 0, recalculates initialEnergy
      mouseleave: Cancels any drag
      dblclick: Resets camera to center/zoom=1

    Init: reset(), bind example-list click handler (loads preset, closes dialog), panic.notice

  _physics-body.lib.js — Body class (ES module, default export):
    constructor(x, y, vx, vy, mass, color='#fff'): Position, velocity, mass, color. Acceleration ax/ay=0. Trail array, maxTrail=500
    recordTrail(): Pushes {x,y}, shifts if > maxTrail
    clearTrail(): Resets trail=[]

  _physics-euler.lib.js — EulerEngine class (ES module, default export):
    Constants: G=1.0, SOFTENING=0.5
    constructor(options={}): g, softening from options or defaults
    calculateAcceleration(body, bodies): N-body gravitational force with softening. accel = G*other.mass/distSq, normalized by dist
    step(bodies, dt): First-order Euler: v += a*dt, x += v*dt. Records trail per body
    Energy methods: kineticEnergy, potentialEnergy, totalEnergy
    Properties: Fast but accumulates energy error over time

  _physics-rk4.lib.js — RK4Engine class (ES module, default export):
    Constants: G=1.0, SOFTENING=0.5
    constructor(options={}): g, softening from options or defaults
    accelerationAt(x, y, others): Computes acceleration at arbitrary position from other bodies {x,y,mass}
    step(bodies, dt): Full RK4 integration using Float64Array state vectors [x,y,vx,vy per body]. Computes k1/k2/k3/k4 via _derivative(), combines with standard RK4 weights (1+2+2+1)/6. Stores final acceleration, records trail
    _derivative(state, bodies, out): Builds virtual body positions from state array, computes acceleration for each
    Energy methods: kineticEnergy, potentialEnergy, totalEnergy
    Properties: 4th order accuracy, excellent energy conservation

  _physics-verlet.lib.js — VerletEngine class (ES module, default export):
    Constants: G=1.0, SOFTENING=0.5
    constructor(options={}): g, softening, _initialized=false
    calculateAcceleration(body, bodies): Same as Euler version
    initialize(bodies): Computes initial accelerations, sets _initialized=true
    step(bodies, dt): Velocity Verlet 3-step: position update (x += v*dt + 0.5*a*dt^2), new acceleration, velocity update (v += 0.5*(a_old+a_new)*dt). Records trail
    Energy methods: kineticEnergy, potentialEnergy, totalEnergy
    reset(): Sets _initialized=false
    Properties: Symplectic (preserves phase space volume), near-perfect long-term energy conservation

  _preset.lib.js — Preset configurations (ES module, named exports):
    COLORS array: ['#e74c3c', '#3498db', '#2ecc71', '#f1c40f', '#9b59b6']
    createPresets(width, height): Factory returning object with 11 preset functions (cx=width/2, cy=height/2):
      random(): 3 bodies at 120deg intervals, random offset, dist 80-140px, mass 80-120, orbital velocity 0.8-1.2
      figure8(): Chenciner-Montgomery (2000) solution. 3 equal masses (100), scale=100. From Moore (1993) coordinates: x1=0.97000436, y1=0.24308753, vx3=0.93240737, vy3=0.86473146
      lagrange(): Equilateral triangle (1772). 3 equal masses (100), r=120, v=sqrt(mass/(sqrt(3)*r)), 120deg intervals
      binary(): Two stars (mass 150, #f1c40f/#e67e22) at cx+/-40, opposing velocities +/-0.8. Planet (mass 5) at cy-180, vx=1.6
      chaos(): Three equal masses (100) in asymmetric triangle, tiny velocity difference (0.5001 vs 0.5)
      euler(): Collinear solution (1767). Three equal masses (100), dist=120, omega=0.004, v=omega*dist
      butterfly(): Suvakov & Dmitrasinovic (2013). Equal masses (100), scale=100, vScale=0.1. vx=0.306893, vy=-0.125507
      moth(): Same setup. vx=0.464445, vy=-0.39606
      yarn(): Same setup. vx=0.050029, vy=-0.640755
      yinyang(): Same setup. vx=0.513938, vy=-0.304736
      dragonfly(): Same setup. vx=0.080584, vy=-0.588836
    All choreography presets (butterfly/moth/yarn/yinyang/dragonfly): Body1 at cx-scale, Body2 at cx+scale, Body3 at cx — Body3 velocity = -2x the other two (momentum conservation)

Important implementation notes:
  - Canvas fixed 1000x1000 (NO devicePixelRatio scaling)
  - Simulation loop uses requestAnimationFrame (unlike solar-system which uses setInterval)
  - All 3 physics engines share the same interface: step(bodies, dt), totalEnergy(bodies)
  - All engines use G=1.0 by default, but createEngine passes softening=2.0 (not default 0.5)
  - Camera system: pan with middle-click or Shift+left-click, reset with double-click. Wheel zoom disabled (commented out)
  - Body drag only when paused: moves body, resets velocity to 0, recalculates energy
  - Collision merging conserves momentum, recalculates initial energy, stops sim if <=1 body
  - Escape detection: positive total energy + distance from CoM > 40% canvas width
  - Energy drift displayed as percentage of initial energy, color-coded (green <1%, red otherwise)
  - Default engine: verlet, default preset: random
  - No auto-start — reset() renders initial state, waits for Play click
  - Examples shown in a modal dialog (examples.modal.md), clicking an item loads preset and closes dialog

Page entièrement générée et maintenue par IA, sans intervention humaine.

Examples
Classical Solutions
Lagrange (1772)
Three equal masses in rotating equilateral triangle
Euler (1767)
Collinear configuration - three bodies in a line
Choreographies
Figure-8
Moore (1993) / Chenciner-Montgomery (2000)
Butterfly
Šuvakov & Dmitrašinović (2013)
Moth
Šuvakov & Dmitrašinović (2013)
Yarn
Šuvakov & Dmitrašinović (2013)
Yin-Yang
Šuvakov & Dmitrašinović (2013)
Dragonfly
Li & Liao (2017)
Demonstrations
Binary + Planet
Two stars with a distant orbiting planet
Chaos
Sensitive dependence on initial conditions
Random
Random configuration with orbital velocities