eldiron-creator 0.9.7

A game creator for classical RPGs.
Documentation
use crate::prelude::*;
use rusterix::ParticleEmitter;
use vek::Vec3;

const PREVIEW_PARTICLE_SIM_FPS: f32 = 15.0;
const PREVIEW_PARTICLE_TIME_SCALE: f32 = 1.0;

pub fn average_rgba_color(pixels: &[u8]) -> [u8; 4] {
    if pixels.is_empty() {
        return [255, 255, 255, 255];
    }
    let mut sum = [0_u64; 4];
    let mut count = 0_u64;
    for rgba in pixels.chunks_exact(4) {
        if rgba[3] == 0 {
            continue;
        }
        sum[0] += rgba[0] as u64;
        sum[1] += rgba[1] as u64;
        sum[2] += rgba[2] as u64;
        sum[3] += rgba[3] as u64;
        count += 1;
    }
    if count == 0 {
        return [255, 176, 72, 255];
    }
    let avg = [
        (sum[0] / count) as u8,
        (sum[1] / count) as u8,
        (sum[2] / count) as u8,
        (sum[3] / count) as u8,
    ];
    if avg[0] < 12 && avg[1] < 12 && avg[2] < 12 {
        [255, 176, 72, 255]
    } else {
        avg
    }
}

pub fn dominant_light_rgba_color(pixels: &[u8]) -> [u8; 4] {
    if pixels.is_empty() {
        return [255, 176, 72, 255];
    }

    let mut max_luma = 0.0f32;
    for rgba in pixels.chunks_exact(4) {
        if rgba[3] == 0 {
            continue;
        }
        let luma = 0.2126 * rgba[0] as f32 + 0.7152 * rgba[1] as f32 + 0.0722 * rgba[2] as f32;
        max_luma = max_luma.max(luma);
    }

    if max_luma <= 1.0 {
        return average_rgba_color(pixels);
    }

    let threshold = (max_luma * 0.45).max(24.0);
    let mut sum = [0.0f32; 4];
    let mut weight_sum = 0.0f32;
    for rgba in pixels.chunks_exact(4) {
        if rgba[3] == 0 {
            continue;
        }
        let luma = 0.2126 * rgba[0] as f32 + 0.7152 * rgba[1] as f32 + 0.0722 * rgba[2] as f32;
        if luma < threshold {
            continue;
        }
        let weight = (luma / 255.0).powf(1.5) * (rgba[3] as f32 / 255.0);
        sum[0] += rgba[0] as f32 * weight;
        sum[1] += rgba[1] as f32 * weight;
        sum[2] += rgba[2] as f32 * weight;
        sum[3] += rgba[3] as f32 * weight;
        weight_sum += weight;
    }

    if weight_sum <= 0.0 {
        return average_rgba_color(pixels);
    }

    [
        (sum[0] / weight_sum).clamp(0.0, 255.0) as u8,
        (sum[1] / weight_sum).clamp(0.0, 255.0) as u8,
        (sum[2] / weight_sum).clamp(0.0, 255.0) as u8,
        (sum[3] / weight_sum).clamp(0.0, 255.0) as u8,
    ]
}

pub fn render_particle_output_preview(
    source_pixels: &[u8],
    particle: &shared::tilegraph::TileParticleOutput,
    width: i32,
    height: i32,
    time: f32,
) -> TheRGBABuffer {
    render_particle_preview_common(
        average_rgba_color(source_pixels),
        Some(&particle.ramp_colors),
        particle.rate,
        particle.spread,
        particle.lifetime_min,
        particle.lifetime_max,
        particle.radius_min,
        particle.radius_max,
        particle.speed_min,
        particle.speed_max,
        particle.flame_base,
        particle.color_variation,
        width,
        height,
        time,
    )
}

pub fn render_particle_emitter_preview(
    emitter: &rusterix::ParticleEmitter,
    width: i32,
    height: i32,
    time: f32,
) -> TheRGBABuffer {
    render_particle_preview_common(
        emitter.color,
        emitter.color_ramp.as_ref(),
        emitter.rate,
        emitter.spread,
        emitter.lifetime_range.0,
        emitter.lifetime_range.1,
        emitter.radius_range.0,
        emitter.radius_range.1,
        emitter.speed_range.0,
        emitter.speed_range.1,
        emitter.flame_base,
        emitter.color_variation,
        width,
        height,
        time,
    )
}

fn render_particle_preview_common(
    base: [u8; 4],
    ramp: Option<&[[u8; 4]; 4]>,
    rate: f32,
    spread: f32,
    lifetime_min: f32,
    lifetime_max: f32,
    radius_min: f32,
    radius_max: f32,
    speed_min: f32,
    speed_max: f32,
    flame_base: bool,
    color_variation: u8,
    width: i32,
    height: i32,
    time: f32,
) -> TheRGBABuffer {
    let width = width.max(1);
    let height = height.max(1);
    let mut preview = TheRGBABuffer::new(TheDim::sized(width, height));
    preview.fill([10, 12, 16, 255]);

    let ramp = ramp.copied().unwrap_or_else(|| derive_particle_ramp(base));
    let glow = [
        ((ramp[1][0] as f32) * 0.35 + 24.0).clamp(0.0, 255.0) as u8,
        ((ramp[1][1] as f32) * 0.35 + 18.0).clamp(0.0, 255.0) as u8,
        ((ramp[1][2] as f32) * 0.35 + 20.0).clamp(0.0, 255.0) as u8,
        255,
    ];
    for y in 0..height {
        let t = y as f32 / height.max(1) as f32;
        let row = [
            ((glow[0] as f32) * (1.0 - t) + 6.0 * t) as u8,
            ((glow[1] as f32) * (1.0 - t) + 8.0 * t) as u8,
            ((glow[2] as f32) * (1.0 - t) + 14.0 * t) as u8,
            255,
        ];
        preview.draw_horizontal_line(0, width - 1, y, row);
    }

    let mut emitter = ParticleEmitter::new(Vec3::zero(), Vec3::new(0.0, 1.0, 0.0));
    emitter.rate = rate.max(0.0);
    emitter.spread = spread.clamp(0.0, std::f32::consts::PI);
    emitter.lifetime_range = (
        lifetime_min.max(0.01),
        lifetime_max.max(lifetime_min.max(0.01)),
    );
    emitter.radius_range = (radius_min.max(0.001), radius_max.max(radius_min.max(0.001)));
    emitter.speed_range = (speed_min.max(0.0), speed_max.max(speed_min.max(0.0)));
    emitter.color = ramp[0];
    emitter.color_ramp = Some(ramp);
    emitter.color_variation = color_variation;

    let steps = ((time.max(0.0) / (1.0 / PREVIEW_PARTICLE_SIM_FPS)).ceil() as usize).max(1);
    let dt = (time.max(0.0) / steps as f32) * PREVIEW_PARTICLE_TIME_SCALE;
    for _ in 0..steps {
        emitter.update(dt);
    }

    let emitter_x = width as f32 * 0.5;
    let emitter_y = height as f32 * 0.88;
    if flame_base {
        draw_soft_particle(
            &mut preview,
            emitter_x,
            emitter_y - 5.0,
            (((radius_min + radius_max) * 0.5) * 36.0).clamp(12.0, 34.0),
            [ramp[1][0], ramp[1][1], ramp[1][2], 245],
        );
        draw_soft_particle(
            &mut preview,
            emitter_x,
            emitter_y - 8.0,
            (((radius_min + radius_max) * 0.5) * 24.0).clamp(8.0, 22.0),
            [ramp[0][0], ramp[0][1], ramp[0][2], 245],
        );
    }
    for particle in &emitter.particles {
        let x = emitter_x + particle.pos.x * width as f32 * 0.28;
        let y = emitter_y - particle.pos.y * height as f32 * 0.36;
        let size = (particle.radius * 34.0).clamp(4.0, 42.0);
        let alpha = (particle.lifetime / particle.initial_lifetime.max(0.001)).clamp(0.0, 1.0);
        draw_soft_particle(
            &mut preview,
            x,
            y,
            size,
            [
                particle.color[0],
                particle.color[1],
                particle.color[2],
                (255.0 * alpha) as u8,
            ],
        );
    }

    let emitter_dim = TheDim::new((emitter_x as i32) - 6, (emitter_y as i32) - 6, 12, 12);
    preview.draw_disc(&emitter_dim, &[255, 255, 255, 140], 1.0, &[0, 0, 0, 0]);
    preview
}

fn derive_particle_ramp(base: [u8; 4]) -> [[u8; 4]; 4] {
    [
        [
            (base[0] as f32 * 1.15).clamp(0.0, 255.0) as u8,
            (base[1] as f32 * 1.1).clamp(0.0, 255.0) as u8,
            (base[2] as f32 * 0.9 + 24.0).clamp(0.0, 255.0) as u8,
            255,
        ],
        [
            (base[0] as f32).clamp(0.0, 255.0) as u8,
            (base[1] as f32).clamp(0.0, 255.0) as u8,
            (base[2] as f32).clamp(0.0, 255.0) as u8,
            255,
        ],
        [
            (base[0] as f32 * 0.75).clamp(0.0, 255.0) as u8,
            (base[1] as f32 * 0.45).clamp(0.0, 255.0) as u8,
            (base[2] as f32 * 0.3).clamp(0.0, 255.0) as u8,
            255,
        ],
        [36, 32, 32, 255],
    ]
}

fn draw_soft_particle(
    buffer: &mut TheRGBABuffer,
    center_x: f32,
    center_y: f32,
    radius: f32,
    color: [u8; 4],
) {
    let min_x = (center_x - radius - 1.0).floor() as i32;
    let max_x = (center_x + radius + 1.0).ceil() as i32;
    let min_y = (center_y - radius - 1.0).floor() as i32;
    let max_y = (center_y + radius + 1.0).ceil() as i32;
    let stride = buffer.dim().width;
    let height = buffer.dim().height;

    for y in min_y..=max_y {
        if y < 0 || y >= height {
            continue;
        }
        for x in min_x..=max_x {
            if x < 0 || x >= stride {
                continue;
            }
            let dx = (x as f32 + 0.5 - center_x) / radius.max(0.001);
            let dy = (y as f32 + 0.5 - center_y) / radius.max(0.001);
            let dist = (dx * dx + dy * dy).sqrt();
            if dist > 1.0 {
                continue;
            }
            let falloff = (1.0 - dist).powf(2.0);
            let alpha = (color[3] as f32 / 255.0) * falloff;
            let index = ((y * stride + x) * 4) as usize;
            let dst = &mut buffer.pixels_mut()[index..index + 4];
            let inv = 1.0 - alpha;
            dst[0] = (dst[0] as f32 * inv + color[0] as f32 * alpha) as u8;
            dst[1] = (dst[1] as f32 * inv + color[1] as f32 * alpha) as u8;
            dst[2] = (dst[2] as f32 * inv + color[2] as f32 * alpha) as u8;
            dst[3] = 255;
        }
    }
}