Preview App nbody 示例输出
//! N-body gravitational simulation — a canonical "iterative only" animation:
//! no closed-form solution, chaotic, and inherently stateful across frames.
//!
//! `NBodyState::new(n)` places `n` equal masses on a regular n-gon with
//! tangential velocities (exact circular speed for the relative equilibrium,
//! plus a tiny asymmetry). The logical simulation duration is the local
//! `sim_secs` value captured by the step closure (not a global constant), and
//! the same value is passed to `with_duration`. Each body leaves a fading
//! trail of recent positions; the `Extract` impl projects bodies and trails
//! once per frame. Circles stay semantic and their positions live in
//! `Translation` wrappers applied at extraction time.
//!
//! Behavior by `n`: n = 3 stays inside the frame and wobbles chaotically for
//! the whole scene; n >= 4 destabilizes and ejects a body (a dramatic finale).

use ranim::{
    color::{AlphaColor, Srgb, palettes::manim},
    core::Extract,
    core::animation::eval::iterative::Iterative,
    core::core_item::CoreItem,
    glam::DVec3,
    items::vitem::geometry::Circle,
    prelude::*,
};
use std::f64::consts::PI;

const G: f64 = 8.0;
const TRAIL_SAMPLE_EVERY: usize = 4; // sample a trail point every 4th step (~30 Hz)
const TRAIL_LEN: usize = 90; // ~3 s of trail per body

const PALETTE: [AlphaColor<Srgb>; 6] = [
    manim::BLUE_C,
    manim::RED_C,
    manim::YELLOW_C,
    manim::GREEN_C,
    manim::PURPLE_C,
    manim::ORANGE,
];

#[derive(Clone, Copy)]
struct Body {
    pos: DVec3,
    vel: DVec3,
    mass: f64,
}

/// The N-body system's full state.
#[derive(Clone)]
struct NBodyState {
    bodies: Vec<Body>,
    trails: Vec<Vec<DVec3>>,
    step_count: usize,
    colors: Vec<AlphaColor<Srgb>>,
}

impl NBodyState {
    /// Place `n` equal masses on a regular n-gon.
    ///
    /// The circular speed is exact for the regular n-gon relative equilibrium:
    /// `v² = G·Σ csc(πj/n) / (4R)`. A 4% overspeed plus a tiny asymmetry keeps
    /// the system lively (and chaotic — the n-gon equilibrium is unstable).
    fn new(n: usize) -> Self {
        assert!(n >= 2, "nbody needs at least 2 bodies");
        let radius = 2.0;
        let csc_sum: f64 = (1..n).map(|j| 1.0 / (PI * j as f64 / n as f64).sin()).sum();
        let v_circ = (G * csc_sum / (4.0 * radius)).sqrt();
        let v0 = 1.04 * v_circ;

        let mut bodies = Vec::with_capacity(n);
        for i in 0..n {
            let angle = PI * 2.0 * i as f64 / n as f64;
            let pos = DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0);
            let velocity = v0 * (1.0 + 0.001 * i as f64);
            let vel = DVec3::new(-velocity * angle.sin(), velocity * angle.cos(), 0.0);
            bodies.push(Body {
                pos,
                vel,
                mass: 1.0,
            });
        }

        let colors = (0..n).map(|i| PALETTE[i % PALETTE.len()]).collect();
        Self {
            bodies,
            trails: vec![Vec::new(); n],
            step_count: 0,
            colors,
        }
    }

    fn accelerations(&self) -> Vec<DVec3> {
        let mut accs = vec![DVec3::ZERO; self.bodies.len()];
        for (i, acc) in accs.iter_mut().enumerate() {
            for j in 0..self.bodies.len() {
                if i == j {
                    continue;
                }
                let r = self.bodies[j].pos - self.bodies[i].pos;
                let d2 = r.length_squared();
                let d = d2.sqrt();
                *acc += r * (G * self.bodies[j].mass / (d2 * d));
            }
        }
        accs
    }

    fn step(&mut self, dt: f64) {
        // Velocity Verlet (symplectic): conserves energy far better than
        // semi-implicit Euler for gravitational orbits.
        let a0 = self.accelerations();
        for (i, body) in self.bodies.iter_mut().enumerate() {
            body.pos += body.vel * dt + a0[i] * (0.5 * dt * dt);
        }
        let a1 = self.accelerations();
        for (i, body) in self.bodies.iter_mut().enumerate() {
            body.vel += (a0[i] + a1[i]) * (0.5 * dt);
        }

        self.step_count += 1;
        if self.step_count.is_multiple_of(TRAIL_SAMPLE_EVERY) {
            for (i, trail) in self.trails.iter_mut().enumerate() {
                trail.push(self.bodies[i].pos);
                if trail.len() > TRAIL_LEN {
                    trail.remove(0);
                }
            }
        }
    }
}

impl Extract for NBodyState {
    type Target = CoreItem;

    fn extract_into(&self, buf: &mut Vec<CoreItem>) {
        // Trails: dim dots at recent positions.
        for (i, trail) in self.trails.iter().enumerate() {
            for &p in trail {
                let mut dot = Circle::new(0.02).transformed(Translation(p));
                dot.set_fill_color(self.colors[i]);
                dot.set_fill_opacity(0.15);
                dot.set_stroke_opacity(0.0);
                dot.extract_into(buf);
            }
        }
        // Bodies.
        for (i, body) in self.bodies.iter().enumerate() {
            let mut ball = Circle::new(0.32).transformed(Translation(body.pos));
            ball.set_fill_color(self.colors[i]);
            ball.set_stroke_opacity(0.0);
            ball.extract_into(buf);
        }
    }
}

#[scene]
#[wasm_demo_doc]
#[output(dir = "./output/nbody")]
fn nbody(r: &mut RanimScene) {
    let sim_secs = 32.0;
    r.play(CameraFrame::default().show().with_duration(sim_secs));
    // n = 3: chaotic wobble that stays in frame; n >= 4: ejection finale.
    r.play(
        Iterative::from_fn(
            NBodyState::new(3),
            move |state: &mut NBodyState, _alpha: f64, delta_alpha: f64| {
                state.step(sim_secs * delta_alpha);
            },
        )
        .with_steps((sim_secs * 60.0) as usize)
        .with_duration(sim_secs),
    );
    r.insert_time_mark(sim_secs / 2.0, TimeMark::Capture("preview.png".to_string()));
}