proof-engine 0.2.1

Real-time graphics from math: glyphs and particles moved by ODEs, strange attractors and force fields, drawn with HDR bloom on OpenGL.
Documentation

// ── Particle Warm-Up & Pre-Simulation ────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct ParticleWarmUp {
    pub duration_secs: f32,
    pub dt: f32,
    pub enabled: bool,
}

impl ParticleWarmUp {
    pub fn new(duration: f32, dt: f32) -> Self { Self { duration_secs: duration, dt: dt.max(0.001), enabled: true } }
    pub fn steps(&self) -> u32 { (self.duration_secs / self.dt) as u32 }
    pub fn disable(mut self) -> Self { self.enabled = false; self }
    pub fn one_second() -> Self { Self::new(1.0, 0.033) }
}

impl Default for ParticleWarmUp {
    fn default() -> Self { Self::new(0.0, 0.033) }
}

// ── Particle Data Buffer ──────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct ParticleDataBuffer {
    pub positions: Vec<Vec3>,
    pub velocities: Vec<Vec3>,
    pub colors: Vec<Vec4>,
    pub sizes: Vec<f32>,
    pub lifetimes: Vec<f32>,
    pub ages: Vec<f32>,
    pub rotations: Vec<f32>,
    pub capacity: u32,
    pub active_count: u32,
}

impl ParticleDataBuffer {
    pub fn new(capacity: u32) -> Self {
        let n = capacity as usize;
        Self { positions: vec![Vec3::ZERO; n], velocities: vec![Vec3::ZERO; n], colors: vec![Vec4::ONE; n], sizes: vec![1.0; n], lifetimes: vec![1.0; n], ages: vec![0.0; n], rotations: vec![0.0; n], capacity, active_count: 0 }
    }
    pub fn is_alive(&self, idx: usize) -> bool { idx < self.active_count as usize && self.ages[idx] < self.lifetimes[idx] }
    pub fn age_normalized(&self, idx: usize) -> f32 {
        let lt = self.lifetimes[idx];
        if lt <= 0.0 { 1.0 } else { (self.ages[idx] / lt).clamp(0.0, 1.0) }
    }
    pub fn active_count(&self) -> u32 { self.active_count }
    pub fn capacity(&self) -> u32 { self.capacity }
    pub fn utilization(&self) -> f32 { if self.capacity == 0 { 0.0 } else { self.active_count as f32 / self.capacity as f32 } }
    pub fn clear(&mut self) { self.active_count = 0; }
    pub fn tick_ages(&mut self, dt: f32) { for i in 0..self.active_count as usize { self.ages[i] += dt; } }
}

// ── Final constants ───────────────────────────────────────────────────────────

pub const PARTICLE_WARMUP_MAX_SECS: f32 = 30.0;
pub const PARTICLE_DATA_BUFFER_DEFAULT: u32 = 65536;
pub const PARTICLE_MAX_ACTIVE_SYSTEMS: u32 = 128;
pub const PARTICLE_TICK_RATE_DEFAULT: f32 = 60.0;

pub fn particle_data_buffer_size_bytes(capacity: u32) -> u64 {
    let n = capacity as u64;
    n * (12 + 12 + 16 + 4 + 4 + 4 + 4)  // vec3 + vec3 + vec4 + f32*4
}

pub fn describe_blend_mode(mode: &BlendMode) -> &'static str {
    match mode {
        BlendMode::Additive => "Adds particle color to background — bright, glowing look",
        BlendMode::Alpha => "Standard transparency — alpha channel used",
        BlendMode::Multiply => "Darkens background — shadows, stains",
        BlendMode::Screen => "Lightens background — soft glows",
        BlendMode::Premultiplied => "Alpha pre-multiplied — avoids fringing",
    }
}