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.