use crate::{
particle_editor::{hooks::hooks_plugin, spawn::spawn_plugin},
particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2},
};
use bevy::prelude::*;
use bevy_egui::{EguiContexts, EguiPlugin, EguiPrimaryContextPass, egui};
use bevy_rapier2d::plugin::{NoUserData, RapierPhysicsPlugin};
use ordered_float::OrderedFloat;
use ron::ser::PrettyConfig;
use std::collections::BTreeMap;
mod ecs;
mod hooks;
mod spawn;
pub fn particle_editor_plugin(app: &mut App) {
app.add_plugins(DefaultPlugins);
app.add_plugins(EguiPlugin::default());
app.add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(10.0));
app.add_systems(Startup, setup_camera_system);
app.add_systems(EguiPrimaryContextPass, editor_ui);
app.add_systems(Startup, setup_editor_effect);
app.add_plugins(spawn_plugin);
app.add_plugins(hooks_plugin);
app.insert_resource(EditorResource {
ser_field: String::new(),
status: "Ready".to_string(),
add_scale_t: 0.0,
add_scale_v: 0.0,
add_color_t: 0.0,
add_color_v: [0u8; 4],
scale_curve: LifetimeCurve::new(&[(0.0f32, 5.0), (2.0, 0.0)])
.0
.iter()
.map(|u| (u.0.clone(), u.1.clone()))
.collect::<Vec<_>>(),
color_curve: vec![(OrderedFloat(0.0f32), [255, 0, 0, 255])],
});
}
fn setup_editor_effect(mut commands: Commands) {
commands.spawn((
ParticleEffect {
lifetime_seconds: RandF32 {
value: 2.0,
randomness: 0.1,
},
batch_spawn_delay_seconds: RandF32 {
value: 0.1,
randomness: 0.05,
},
particles_in_batch: RandUsize {
value: 1,
randomness: 1,
},
initial_linear_velocity: RandVec2 {
x: RandF32 {
value: 0.0,
randomness: 0.5,
},
y: RandF32 {
value: 10.0,
randomness: 1.0,
},
},
initial_angular_velocity: RandF32 {
value: 1.0,
randomness: 0.5,
},
scale: LifetimeCurve::new(&[(0.0f32, 5.0), (2.0, 0.0)]),
color: LifetimeCurve::new(&[(0.0f32, Srgba::new(1.0, 0.0, 0.0, 1.0).into())]),
},
Transform::from_xyz(0.0, 0.0, 0.0),
));
}
fn setup_camera_system(mut commands: Commands) {
commands.spawn((Camera2d, Transform::from_scale(Vec3::splat(0.1))));
}
#[derive(Resource)]
struct EditorResource {
ser_field: String,
status: String,
add_scale_t: f32,
add_scale_v: f32,
add_color_t: f32,
add_color_v: [u8; 4],
scale_curve: Vec<(OrderedFloat<f32>, f32)>,
color_curve: Vec<(OrderedFloat<f32>, [u8; 4])>,
}
fn editor_ui(
mut contexts: EguiContexts,
effect: Single<&mut ParticleEffect>,
mut editor_resource: ResMut<EditorResource>,
) -> Result {
let mut effect = effect.into_inner();
egui::Window::new("Particle Effect")
.resizable(false)
.show(contexts.ctx_mut()?, |ui| {
egui::Grid::new("effect").striped(true).show(ui, |ui| {
draw_rand_f32(&mut effect.lifetime_seconds, "Lifetime (seconds): ", ui);
draw_rand_f32(
&mut effect.batch_spawn_delay_seconds,
"Delay in between batches (seconds): ",
ui,
);
draw_rand_usize(
&mut effect.particles_in_batch,
"Number of particles in batch: ",
ui,
);
draw_rand_f32(
&mut effect.initial_linear_velocity.x,
"Linear velocity (x-axis, m/s): ",
ui,
);
draw_rand_f32(
&mut effect.initial_linear_velocity.y,
"Linear velocity (y-axis, m/s): ",
ui,
);
draw_rand_f32(
&mut effect.initial_angular_velocity,
"Angular velocity (radians/second): ",
ui,
);
ui.separator();
ui.label("Scale curve");
if ui.button("sort").clicked() {
editor_resource.scale_curve.sort_by_key(|u| u.0);
}
ui.end_row();
editor_resource.scale_curve.retain_mut(|(k, v)| {
ui.label("scale t=");
ui.add(
egui::DragValue::new(k.as_mut())
.speed(0.01)
.range(0.0f32..=effect.lifetime_seconds.value),
);
ui.label("v=");
ui.add(egui::DragValue::new(v).speed(0.01));
let r = ui.button("-");
ui.end_row();
!r.clicked()
});
ui.separator();
ui.end_row();
ui.label("new scale: t=");
ui.add(
egui::DragValue::new(&mut editor_resource.add_scale_t)
.speed(0.01)
.range(0.0f32..=effect.lifetime_seconds.value),
);
ui.label("v=");
ui.add(egui::DragValue::new(&mut editor_resource.add_scale_v).speed(0.01));
if ui.button("+").clicked() {
let new_v = (
OrderedFloat(editor_resource.add_scale_t),
editor_resource.add_scale_v,
)
.clone();
editor_resource.scale_curve.push(new_v);
}
ui.end_row();
effect.scale = LifetimeCurve(BTreeMap::from_iter(
editor_resource.scale_curve.iter().copied(),
));
ui.separator();
ui.end_row();
ui.separator();
ui.label("Color curve");
if ui.button("sort").clicked() {
editor_resource.color_curve.sort_by_key(|u| u.0);
}
ui.end_row();
editor_resource.color_curve.retain_mut(|(k, v)| {
ui.label("color t=");
ui.add(
egui::DragValue::new(k.as_mut())
.speed(0.01)
.range(0.0f32..=effect.lifetime_seconds.value),
);
ui.label("v=");
ui.color_edit_button_srgba_unmultiplied(v);
let r = ui.button("-");
ui.end_row();
!r.clicked()
});
ui.separator();
ui.end_row();
ui.label("new color: t=");
ui.add(
egui::DragValue::new(&mut editor_resource.add_color_t)
.speed(0.01)
.range(0.0f32..=effect.lifetime_seconds.value),
);
ui.label("v=");
ui.color_edit_button_srgba_unmultiplied(&mut editor_resource.add_color_v);
if ui.button("+").clicked() {
let new_v = (
OrderedFloat(editor_resource.add_color_t),
editor_resource.add_color_v,
)
.clone();
editor_resource.color_curve.push(new_v);
}
ui.end_row();
let curve_copied = editor_resource.color_curve.clone();
effect.color =
LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(|(u, v)| {
(
*u,
Color::Srgba(Srgba::new(
v[0] as f32 / 256.0,
v[1] as f32 / 256.0,
v[2] as f32 / 256.0,
v[3] as f32 / 256.0,
)),
)
})));
ui.separator();
ui.end_row();
});
ui.horizontal(|ui| {
if ui.button("Generate").clicked() {
effect.scale = LifetimeCurve(BTreeMap::from_iter(
editor_resource.scale_curve.iter().copied(),
));
let curve_copied = editor_resource.color_curve.clone();
effect.color =
LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(|(u, v)| {
(
*u,
Color::Srgba(Srgba::new(
v[0] as f32 / 256.0,
v[1] as f32 / 256.0,
v[2] as f32 / 256.0,
v[3] as f32 / 256.0,
)),
)
})));
editor_resource.ser_field = ron::ser::to_string(effect.as_ref()).unwrap();
editor_resource.status = "Ready; Generated OK".to_string();
}
if ui.button("Load").clicked() {
match ron::from_str(&editor_resource.ser_field) {
Ok(e) => {
*effect = e;
editor_resource.scale_curve = effect
.scale
.0
.iter()
.map(|u| (u.0.clone(), u.1.clone()))
.collect::<Vec<_>>();
editor_resource.color_curve = effect
.color
.0
.iter()
.map(|u| {
(u.0.clone(), {
let mut r = [0u8; 4];
let srgba: Srgba = (*u.1).into();
r[0] = (srgba.red * 256.0).floor() as u8;
r[1] = (srgba.green * 256.0).floor() as u8;
r[2] = (srgba.blue * 256.0).floor() as u8;
r[3] = (srgba.alpha * 256.0).floor() as u8;
r
})
})
.collect::<Vec<_>>();
}
Err(e) => {
editor_resource.status = e.to_string();
}
};
}
});
ui.text_edit_multiline(&mut editor_resource.ser_field);
ui.text_edit_multiline(&mut editor_resource.status.as_str());
});
Ok(())
}
fn draw_rand_f32(v: &mut RandF32, l: &str, ui: &mut egui::Ui) {
ui.label(l);
ui.add(egui::DragValue::new(&mut v.value).speed(0.01));
ui.label("variance:");
ui.add(egui::DragValue::new(&mut v.randomness).speed(0.01));
ui.end_row();
}
fn draw_rand_usize(v: &mut RandUsize, l: &str, ui: &mut egui::Ui) {
ui.label(l);
ui.add(egui::DragValue::new(&mut v.value).speed(0.1));
ui.label("variance:");
ui.add(egui::DragValue::new(&mut v.randomness).speed(0.1));
ui.end_row();
}