
๐ Rust Code
use nannou::prelude::*;
#[derive(Debug)]
struct Model {
window: WindowId,
time: f32,
frame_count: u64,
mouse_pos: Point2,
mouse_pressed: bool,
keys_pressed: std::collections::HashSet<Key>,
is_paused: bool,
background_color: Rgb<u8>,
particles: Vec<Particle>,
}
#[derive(Debug, Clone)]
struct Particle {
position: Point2,
velocity: Vec2,
color: Rgb<u8>,
size: f32,
life: f32,
}
impl Particle {
fn new(pos: Point2) -> Self {
Self {
position: pos,
velocity: vec2(random_range(-2.0, 2.0), random_range(-2.0, 2.0)),
color: rgb(
random_range(0, 255),
random_range(0, 255),
random_range(0, 255),
),
size: random_range(2.0, 8.0),
life: 1.5,
}
}
fn update(&mut self) {
self.position += self.velocity;
self.life -= 0.005;
self.velocity *= 0.99;
}
fn is_alive(&self) -> bool {
self.life > 0.0
}
}
fn main() {
nannou::app(model).update(update).event(event).run();
}
fn model(app: &App) -> Model {
let window_id = app
.new_window()
.title("Fireworks Generator")
.size(800, 800)
.view(view)
.build()
.unwrap();
Model {
window: window_id,
time: 0.0,
frame_count: 0,
mouse_pos: pt2(0.0, 0.0),
mouse_pressed: false,
keys_pressed: std::collections::HashSet::new(),
is_paused: false,
background_color: rgb(20, 20, 30),
particles: Vec::new(),
}
}
fn update(app: &App, model: &mut Model, _update: Update) {
if model.is_paused {
return;
}
model.time = app.time;
model.frame_count += 1;
model.mouse_pos = app.mouse.position();
for particle in &mut model.particles {
particle.update();
}
model.particles.retain(|p| p.is_alive());
if model.mouse_pressed && model.frame_count % 2 == 0 {
model.particles.push(Particle::new(model.mouse_pos));
}
if model.particles.len() > 500 {
model.particles.drain(0..100);
}
}
fn event(app: &App, model: &mut Model, event: Event) {
match event {
Event::WindowEvent {
simple: Some(event),
..
} => {
match event {
KeyPressed(key) => {
model.keys_pressed.insert(key);
handle_key_pressed(app, model, key);
}
KeyReleased(key) => {
model.keys_pressed.remove(&key);
}
MousePressed(_button) => {
model.mouse_pressed = true;
}
MouseReleased(_button) => {
model.mouse_pressed = false;
}
MouseMoved(_pos) => {
}
Resized(_size) => {
}
_ => {}
}
}
_ => {}
}
}
fn handle_key_pressed(app: &App, model: &mut Model, key: Key) {
match key {
Key::Space => {
model.is_paused = !model.is_paused;
}
Key::C => {
model.particles.clear();
}
Key::R => {
model.background_color = rgb(
random_range(0, 255),
random_range(0, 255),
random_range(0, 255),
);
}
Key::S => {
let file_path = app
.project_path()
.expect("Cannot find project path")
.join("screenshots")
.join(format!("screenshot_{}.png", model.frame_count));
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).ok();
}
app.window(model.window)
.expect("Cannot find window")
.capture_frame(file_path);
}
Key::Escape => {
app.quit();
}
_ => {}
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(model.background_color);
for particle in &model.particles {
let alpha = particle.life;
let color = rgba(
particle.color.red as f32 / 255.0,
particle.color.green as f32 / 255.0,
particle.color.blue as f32 / 255.0,
alpha,
);
draw.ellipse()
.xy(particle.position)
.radius(particle.size)
.color(color);
}
let win = app.window_rect();
draw.rect()
.xy(pt2(win.left() + 100.0, win.top() - 60.0))
.w_h(180.0, 90.0)
.color(rgba(0.0, 0.0, 0.0, 0.7));
draw.text(&format!("Frame: {}", model.frame_count))
.xy(pt2(win.left() + 20.0, win.top() - 30.0))
.font_size(16)
.color(WHITE);
draw.text(&format!("Particles: {}", model.particles.len()))
.xy(pt2(win.left() + 20.0, win.top() - 50.0))
.font_size(16)
.color(WHITE);
draw.text(&format!("Time: {:.2}s", model.time))
.xy(pt2(win.left() + 20.0, win.top() - 70.0))
.font_size(16)
.color(WHITE);
if model.is_paused {
draw.text("PAUSED")
.xy(pt2(0.0, 0.0))
.font_size(48)
.color(RED);
}
let guide_text = [
"SPACE: Play/Pause",
"C: Clear particles",
"R: Random background",
"S: Save screenshot",
"Mouse: Create particles",
"ESC: Exit",
];
draw.rect()
.xy(pt2(win.right() - 120.0, win.bottom() + 70.0))
.w_h(220.0, 130.0)
.color(rgba(0.0, 0.0, 0.0, 0.7));
for (i, text) in guide_text.iter().enumerate() {
draw.text(text)
.xy(pt2(
win.right() - 220.0,
win.bottom() + 20.0 + (i as f32 * 20.0),
))
.font_size(14)
.color(WHITE);
}
draw.to_frame(app, &frame).unwrap();
}